oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.424d8f9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +945 -21
  2. package/dist/index.js +1503 -229
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -890,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
890
890
  });
891
891
  //#endregion
892
892
  //#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
893
- const stripCommentsPreservingPositions = (content) => {
893
+ const WHITESPACE_PATTERN = /\s/;
894
+ const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
895
+ for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
896
+ const character = content[cursor];
897
+ if (character === "\\") {
898
+ cursor += 1;
899
+ continue;
900
+ }
901
+ if (character === delimiter) return false;
902
+ if (WHITESPACE_PATTERN.test(character)) return true;
903
+ }
904
+ return false;
905
+ };
906
+ const blankNonCodePreservingPositions = (content, blankStringContents) => {
894
907
  const characters = content.split("");
895
908
  let stringDelimiter = null;
909
+ let isBlankingString = false;
910
+ const templateExpressionDepths = [];
896
911
  let index = 0;
912
+ const blankUnlessNewline = (offset) => {
913
+ if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
914
+ };
897
915
  while (index < content.length) {
898
916
  const character = content[index];
899
917
  const nextCharacter = content[index + 1];
900
918
  if (stringDelimiter !== null) {
901
919
  if (character === "\\") {
920
+ if (isBlankingString) {
921
+ blankUnlessNewline(index);
922
+ blankUnlessNewline(index + 1);
923
+ }
902
924
  index += 2;
903
925
  continue;
904
926
  }
905
- if (character === stringDelimiter) stringDelimiter = null;
927
+ if (character === stringDelimiter) {
928
+ stringDelimiter = null;
929
+ index += 1;
930
+ continue;
931
+ }
932
+ if (blankStringContents && stringDelimiter === "`" && character === "$" && nextCharacter === "{") {
933
+ templateExpressionDepths.push(0);
934
+ stringDelimiter = null;
935
+ index += 2;
936
+ continue;
937
+ }
938
+ if (isBlankingString) blankUnlessNewline(index);
906
939
  index += 1;
907
940
  continue;
908
941
  }
909
- if (character === "\"" || character === "'" || character === "`") {
942
+ if (character === "\"" || character === "'") {
910
943
  stringDelimiter = character;
944
+ isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
945
+ index += 1;
946
+ continue;
947
+ }
948
+ if (character === "`") {
949
+ stringDelimiter = "`";
950
+ isBlankingString = blankStringContents;
911
951
  index += 1;
912
952
  continue;
913
953
  }
@@ -926,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
926
966
  index += 2;
927
967
  break;
928
968
  }
929
- if (content[index] !== "\n") characters[index] = " ";
969
+ blankUnlessNewline(index);
930
970
  index += 1;
931
971
  }
932
972
  continue;
933
973
  }
974
+ if (templateExpressionDepths.length > 0) {
975
+ const innermost = templateExpressionDepths.length - 1;
976
+ if (character === "{") templateExpressionDepths[innermost] += 1;
977
+ else if (character === "}") if (templateExpressionDepths[innermost] === 0) {
978
+ templateExpressionDepths.pop();
979
+ stringDelimiter = "`";
980
+ isBlankingString = blankStringContents;
981
+ } else templateExpressionDepths[innermost] -= 1;
982
+ }
934
983
  index += 1;
935
984
  }
936
985
  return characters.join("");
937
986
  };
987
+ const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
988
+ const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
938
989
  //#endregion
939
990
  //#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
940
991
  const strippedContentCache = /* @__PURE__ */ new WeakMap();
941
- const getScannableContent = (file) => {
992
+ const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
993
+ const getScannableContent = (file, ignoreStringLiterals = false) => {
942
994
  if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
943
- const cachedContent = strippedContentCache.get(file);
995
+ const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
996
+ const cachedContent = cache.get(file);
944
997
  if (cachedContent !== void 0) return cachedContent;
945
- const strippedContent = stripCommentsPreservingPositions(file.content);
946
- strippedContentCache.set(file, strippedContent);
998
+ const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
999
+ cache.set(file, strippedContent);
947
1000
  return strippedContent;
948
1001
  };
949
- const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
1002
+ const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
950
1003
  if (!shouldScan(file)) return [];
951
- const content = getScannableContent(file);
1004
+ const content = getScannableContent(file, ignoreStringLiterals);
952
1005
  if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
953
1006
  const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
954
1007
  if (matchedPattern === void 0) return [];
@@ -973,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
973
1026
  shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
974
1027
  pattern: AGENT_TOOL_DEFINITION_PATTERN,
975
1028
  requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
1029
+ ignoreStringLiterals: true,
976
1030
  message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
977
1031
  })
978
1032
  });
@@ -1861,7 +1915,7 @@ const anchorAmbiguousText = defineRule({
1861
1915
  });
1862
1916
  //#endregion
1863
1917
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1864
- const MESSAGE$51 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1918
+ const MESSAGE$64 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1865
1919
  const anchorHasContent = defineRule({
1866
1920
  id: "anchor-has-content",
1867
1921
  title: "Anchor has no content",
@@ -1877,7 +1931,7 @@ const anchorHasContent = defineRule({
1877
1931
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1878
1932
  context.report({
1879
1933
  node: opening.name,
1880
- message: MESSAGE$51
1934
+ message: MESSAGE$64
1881
1935
  });
1882
1936
  } })
1883
1937
  });
@@ -2271,7 +2325,7 @@ const parseJsxValue = (value) => {
2271
2325
  };
2272
2326
  //#endregion
2273
2327
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
2274
- const MESSAGE$50 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2328
+ const MESSAGE$63 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2275
2329
  const ariaActivedescendantHasTabindex = defineRule({
2276
2330
  id: "aria-activedescendant-has-tabindex",
2277
2331
  title: "aria-activedescendant missing tabindex",
@@ -2289,14 +2343,14 @@ const ariaActivedescendantHasTabindex = defineRule({
2289
2343
  if (tabIndexValue === null || tabIndexValue >= -1) return;
2290
2344
  context.report({
2291
2345
  node: node.name,
2292
- message: MESSAGE$50
2346
+ message: MESSAGE$63
2293
2347
  });
2294
2348
  return;
2295
2349
  }
2296
2350
  if (isInteractiveElement(tag, node)) return;
2297
2351
  context.report({
2298
2352
  node: node.name,
2299
- message: MESSAGE$50
2353
+ message: MESSAGE$63
2300
2354
  });
2301
2355
  } })
2302
2356
  });
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
3085
3139
  scan: scanByPattern({
3086
3140
  shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
3087
3141
  pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
3088
- requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore|createClient)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket|supabase|SUPABASE_URL)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b/i],
3142
+ requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
3089
3143
  message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
3090
3144
  })
3091
3145
  });
@@ -3104,6 +3158,76 @@ const AUTH_FUNCTION_NAMES = new Set([
3104
3158
  "getAuth",
3105
3159
  "validateSession"
3106
3160
  ]);
3161
+ const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
3162
+ const AUTH_STANDALONE_NOUN_TOKENS = new Set([
3163
+ "signedin",
3164
+ "loggedin",
3165
+ "signin"
3166
+ ]);
3167
+ const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
3168
+ "require",
3169
+ "ensure",
3170
+ "assert",
3171
+ "verify",
3172
+ "validate",
3173
+ "check",
3174
+ "protect",
3175
+ "enforce",
3176
+ "guard",
3177
+ "gate",
3178
+ "restrict",
3179
+ "is",
3180
+ "has",
3181
+ "can",
3182
+ "must"
3183
+ ]);
3184
+ const AUTH_GETTER_VERB_TOKENS = new Set([
3185
+ "get",
3186
+ "fetch",
3187
+ "load",
3188
+ "read",
3189
+ "resolve",
3190
+ "retrieve",
3191
+ "use"
3192
+ ]);
3193
+ const AUTH_QUALIFIER_TOKENS = new Set([
3194
+ "current",
3195
+ "my",
3196
+ "own"
3197
+ ]);
3198
+ const AUTH_STRONG_NOUN_TOKENS = new Set([
3199
+ "session",
3200
+ "sessions",
3201
+ "login",
3202
+ "admin",
3203
+ "admins",
3204
+ "superadmin",
3205
+ "superuser",
3206
+ "role",
3207
+ "roles",
3208
+ "permission",
3209
+ "permissions",
3210
+ "jwt",
3211
+ "identity",
3212
+ "principal",
3213
+ "credential",
3214
+ "credentials"
3215
+ ]);
3216
+ const AUTH_WEAK_NOUN_TOKENS = new Set([
3217
+ "user",
3218
+ "users",
3219
+ "account",
3220
+ "accounts",
3221
+ "token",
3222
+ "tokens",
3223
+ "access",
3224
+ "me",
3225
+ "viewer",
3226
+ "caller",
3227
+ "subject",
3228
+ "scope",
3229
+ "scopes"
3230
+ ]);
3107
3231
  const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
3108
3232
  const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
3109
3233
  const SECRET_PATTERNS = [
@@ -4201,6 +4325,58 @@ const asyncParallel = defineRule({
4201
4325
  }
4202
4326
  });
4203
4327
  //#endregion
4328
+ //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4329
+ const MESSAGE$62 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
4330
+ const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4331
+ const STORAGE_GLOBALS = new Set([
4332
+ "window",
4333
+ "globalThis",
4334
+ "self"
4335
+ ]);
4336
+ const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
4337
+ const isWebStorageObject = (node) => {
4338
+ if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
4339
+ if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
4340
+ return false;
4341
+ };
4342
+ const staticMemberName = (member) => {
4343
+ if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
4344
+ if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
4345
+ return null;
4346
+ };
4347
+ const authTokenInWebStorage = defineRule({
4348
+ id: "auth-token-in-web-storage",
4349
+ title: "Auth token in web storage",
4350
+ severity: "warn",
4351
+ recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
4352
+ create: (context) => ({
4353
+ CallExpression(node) {
4354
+ const callee = node.callee;
4355
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
4356
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
4357
+ if (!isWebStorageObject(callee.object)) return;
4358
+ const keyArgument = node.arguments?.[0];
4359
+ if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
4360
+ if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4361
+ context.report({
4362
+ node,
4363
+ message: MESSAGE$62
4364
+ });
4365
+ },
4366
+ AssignmentExpression(node) {
4367
+ const target = node.left;
4368
+ if (!isNodeOfType(target, "MemberExpression")) return;
4369
+ if (!isWebStorageObject(target.object)) return;
4370
+ const propertyName = staticMemberName(target);
4371
+ if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4372
+ context.report({
4373
+ node: target,
4374
+ message: MESSAGE$62
4375
+ });
4376
+ }
4377
+ })
4378
+ });
4379
+ //#endregion
4204
4380
  //#region src/plugin/rules/a11y/autocomplete-valid.ts
4205
4381
  const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
4206
4382
  const AUTOFILL_TOKENS = new Set([
@@ -4572,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
4572
4748
  //#endregion
4573
4749
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4574
4750
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4575
- const MESSAGE$49 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4751
+ const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4576
4752
  const KEY_HANDLERS = [
4577
4753
  "onKeyUp",
4578
4754
  "onKeyDown",
@@ -4604,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
4604
4780
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4605
4781
  context.report({
4606
4782
  node: node.name,
4607
- message: MESSAGE$49
4783
+ message: MESSAGE$61
4608
4784
  });
4609
4785
  } };
4610
4786
  }
@@ -4719,7 +4895,7 @@ const isReactComponentName = (name) => {
4719
4895
  };
4720
4896
  //#endregion
4721
4897
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
4722
- const MESSAGE$48 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4898
+ const MESSAGE$60 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4723
4899
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4724
4900
  const DEFAULT_LABELLING_PROPS = [
4725
4901
  "alt",
@@ -4880,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
4880
5056
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
4881
5057
  context.report({
4882
5058
  node: opening,
4883
- message: MESSAGE$48
5059
+ message: MESSAGE$60
4884
5060
  });
4885
5061
  } };
4886
5062
  }
@@ -5009,6 +5185,7 @@ const dangerousHtmlSink = defineRule({
5009
5185
  return findings;
5010
5186
  }
5011
5187
  });
5188
+ const WCAG_CONTRAST_NORMAL_MIN = 4.5;
5012
5189
  const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
5013
5190
  const VAGUE_BUTTON_LABELS = new Set([
5014
5191
  "continue",
@@ -5306,6 +5483,38 @@ const noVagueButtonLabel = defineRule({
5306
5483
  } })
5307
5484
  });
5308
5485
  //#endregion
5486
+ //#region src/plugin/utils/has-jsx-spread-attribute.ts
5487
+ const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5488
+ //#endregion
5489
+ //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5490
+ const MESSAGE$59 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5491
+ const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5492
+ const NAME_PROVIDING_ATTRIBUTES = [
5493
+ "aria-label",
5494
+ "aria-labelledby",
5495
+ "title"
5496
+ ];
5497
+ const dialogHasAccessibleName = defineRule({
5498
+ id: "dialog-has-accessible-name",
5499
+ title: "Dialog without accessible name",
5500
+ severity: "warn",
5501
+ recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
5502
+ create: (context) => ({ JSXOpeningElement(node) {
5503
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
5504
+ const tagName = node.name.name;
5505
+ if (tagName[0] !== tagName[0]?.toLowerCase()) return;
5506
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5507
+ const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5508
+ if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5509
+ if (hasJsxSpreadAttribute(node.attributes)) return;
5510
+ if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5511
+ context.report({
5512
+ node: node.name,
5513
+ message: MESSAGE$59
5514
+ });
5515
+ } })
5516
+ });
5517
+ //#endregion
5309
5518
  //#region src/plugin/utils/is-es5-component.ts
5310
5519
  const PRAGMA$2 = "React";
5311
5520
  const CREATE_CLASS = "createReactClass";
@@ -5340,7 +5549,7 @@ const isEs6Component = (node) => {
5340
5549
  };
5341
5550
  //#endregion
5342
5551
  //#region src/plugin/rules/react-builtins/display-name.ts
5343
- const MESSAGE$47 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5552
+ const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5344
5553
  const DEFAULT_ADDITIONAL_HOCS = [
5345
5554
  "observer",
5346
5555
  "lazy",
@@ -5543,7 +5752,7 @@ const displayName = defineRule({
5543
5752
  const reportAt = (node) => {
5544
5753
  context.report({
5545
5754
  node,
5546
- message: MESSAGE$47
5755
+ message: MESSAGE$58
5547
5756
  });
5548
5757
  };
5549
5758
  return {
@@ -7691,7 +7900,7 @@ const forbidElements = defineRule({
7691
7900
  });
7692
7901
  //#endregion
7693
7902
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
7694
- const MESSAGE$46 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7903
+ const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7695
7904
  const forwardRefUsesRef = defineRule({
7696
7905
  id: "forward-ref-uses-ref",
7697
7906
  title: "forwardRef without ref parameter",
@@ -7711,7 +7920,7 @@ const forwardRefUsesRef = defineRule({
7711
7920
  if (isNodeOfType(onlyParam, "RestElement")) return;
7712
7921
  context.report({
7713
7922
  node: inner,
7714
- message: MESSAGE$46
7923
+ message: MESSAGE$57
7715
7924
  });
7716
7925
  } })
7717
7926
  });
@@ -7748,7 +7957,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7748
7957
  });
7749
7958
  //#endregion
7750
7959
  //#region src/plugin/rules/a11y/heading-has-content.ts
7751
- const MESSAGE$45 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7960
+ const MESSAGE$56 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7752
7961
  const DEFAULT_HEADING_TAGS = [
7753
7962
  "h1",
7754
7963
  "h2",
@@ -7781,7 +7990,7 @@ const headingHasContent = defineRule({
7781
7990
  if (isHiddenFromScreenReader(node, context.settings)) return;
7782
7991
  context.report({
7783
7992
  node,
7784
- message: MESSAGE$45
7993
+ message: MESSAGE$56
7785
7994
  });
7786
7995
  } };
7787
7996
  }
@@ -7919,7 +8128,7 @@ const hooksNoNanInDeps = defineRule({
7919
8128
  });
7920
8129
  //#endregion
7921
8130
  //#region src/plugin/rules/a11y/html-has-lang.ts
7922
- const MESSAGE$44 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8131
+ const MESSAGE$55 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
7923
8132
  const resolveSettings$38 = (settings) => {
7924
8133
  const reactDoctor = settings?.["react-doctor"];
7925
8134
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -7967,7 +8176,7 @@ const htmlHasLang = defineRule({
7967
8176
  if (!lang) {
7968
8177
  context.report({
7969
8178
  node: node.name,
7970
- message: MESSAGE$44
8179
+ message: MESSAGE$55
7971
8180
  });
7972
8181
  return;
7973
8182
  }
@@ -7975,13 +8184,13 @@ const htmlHasLang = defineRule({
7975
8184
  if (verdict === "missing" || verdict === "empty") {
7976
8185
  context.report({
7977
8186
  node: lang,
7978
- message: MESSAGE$44
8187
+ message: MESSAGE$55
7979
8188
  });
7980
8189
  return;
7981
8190
  }
7982
8191
  if (hasSpread && !lang) context.report({
7983
8192
  node: node.name,
7984
- message: MESSAGE$44
8193
+ message: MESSAGE$55
7985
8194
  });
7986
8195
  } };
7987
8196
  }
@@ -8195,7 +8404,7 @@ const htmlNoNestedInteractive = defineRule({
8195
8404
  });
8196
8405
  //#endregion
8197
8406
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8198
- const MESSAGE$43 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8407
+ const MESSAGE$54 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8199
8408
  const evaluateTitleValue = (value) => {
8200
8409
  if (!value) return "missing";
8201
8410
  if (isNodeOfType(value, "Literal")) {
@@ -8235,14 +8444,14 @@ const iframeHasTitle = defineRule({
8235
8444
  if (!titleAttr) {
8236
8445
  if (hasSpread || tag === "iframe") context.report({
8237
8446
  node: node.name,
8238
- message: MESSAGE$43
8447
+ message: MESSAGE$54
8239
8448
  });
8240
8449
  return;
8241
8450
  }
8242
8451
  const verdict = evaluateTitleValue(titleAttr.value);
8243
8452
  if (verdict === "missing" || verdict === "empty") context.report({
8244
8453
  node: titleAttr,
8245
- message: MESSAGE$43
8454
+ message: MESSAGE$54
8246
8455
  });
8247
8456
  } })
8248
8457
  });
@@ -8346,7 +8555,7 @@ const iframeMissingSandbox = defineRule({
8346
8555
  });
8347
8556
  //#endregion
8348
8557
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
8349
- const MESSAGE$42 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8558
+ const MESSAGE$53 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8350
8559
  const DEFAULT_COMPONENTS = ["img"];
8351
8560
  const DEFAULT_REDUNDANT_WORDS = [
8352
8561
  "image",
@@ -8411,7 +8620,7 @@ const imgRedundantAlt = defineRule({
8411
8620
  if (!altAttribute) return;
8412
8621
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8413
8622
  node: altAttribute,
8414
- message: MESSAGE$42
8623
+ message: MESSAGE$53
8415
8624
  });
8416
8625
  } };
8417
8626
  }
@@ -10768,7 +10977,7 @@ const jsxMaxDepth = defineRule({
10768
10977
  });
10769
10978
  //#endregion
10770
10979
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
10771
- const MESSAGE$41 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10980
+ const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10772
10981
  const LITERAL_TEXT_TAGS = new Set([
10773
10982
  "code",
10774
10983
  "pre",
@@ -10804,7 +11013,7 @@ const jsxNoCommentTextnodes = defineRule({
10804
11013
  if (isInsideLiteralTextTag(node)) return;
10805
11014
  context.report({
10806
11015
  node,
10807
- message: MESSAGE$41
11016
+ message: MESSAGE$52
10808
11017
  });
10809
11018
  } })
10810
11019
  });
@@ -10835,7 +11044,7 @@ const isInsideFunctionScope = (node) => {
10835
11044
  };
10836
11045
  //#endregion
10837
11046
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
10838
- const MESSAGE$40 = "Every reader of this context redraws on each render because you build its `value` inline.";
11047
+ const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
10839
11048
  const CONTEXT_MODULES$1 = [
10840
11049
  "react",
10841
11050
  "use-context-selector",
@@ -10933,7 +11142,7 @@ const jsxNoConstructedContextValues = defineRule({
10933
11142
  if (!isConstructedValue(innerExpression)) continue;
10934
11143
  context.report({
10935
11144
  node: attribute,
10936
- message: MESSAGE$40
11145
+ message: MESSAGE$51
10937
11146
  });
10938
11147
  }
10939
11148
  }
@@ -11019,7 +11228,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11019
11228
  };
11020
11229
  //#endregion
11021
11230
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
11022
- const MESSAGE$39 = "This child redraws every render because the prop gets brand new JSX each time.";
11231
+ const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
11023
11232
  const KNOWN_SLOT_PROP_NAMES = new Set([
11024
11233
  "icon",
11025
11234
  "Icon",
@@ -11288,7 +11497,7 @@ const jsxNoJsxAsProp = defineRule({
11288
11497
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11289
11498
  context.report({
11290
11499
  node,
11291
- message: MESSAGE$39
11500
+ message: MESSAGE$50
11292
11501
  });
11293
11502
  }
11294
11503
  };
@@ -11576,7 +11785,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11576
11785
  ];
11577
11786
  //#endregion
11578
11787
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
11579
- const MESSAGE$38 = "This child redraws every render because the prop gets a brand new array each time.";
11788
+ const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
11580
11789
  const isDataArrayPropName = (propName) => {
11581
11790
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11582
11791
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11660,7 +11869,7 @@ const jsxNoNewArrayAsProp = defineRule({
11660
11869
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11661
11870
  context.report({
11662
11871
  node,
11663
- message: MESSAGE$38
11872
+ message: MESSAGE$49
11664
11873
  });
11665
11874
  }
11666
11875
  };
@@ -11918,7 +12127,7 @@ const SAFE_RECEIVER_NAMES = new Set([
11918
12127
  ]);
11919
12128
  //#endregion
11920
12129
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
11921
- const MESSAGE$37 = "This child redraws every render because the prop gets a brand new function each time.";
12130
+ const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
11922
12131
  const isAccessorPredicateName = (propName) => {
11923
12132
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
11924
12133
  if (propName.length <= prefix.length) continue;
@@ -12124,7 +12333,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12124
12333
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12125
12334
  context.report({
12126
12335
  node,
12127
- message: MESSAGE$37
12336
+ message: MESSAGE$48
12128
12337
  });
12129
12338
  }
12130
12339
  };
@@ -12344,7 +12553,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12344
12553
  ];
12345
12554
  //#endregion
12346
12555
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
12347
- const MESSAGE$36 = "This child redraws every render because the prop gets a brand new object each time.";
12556
+ const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
12348
12557
  const isConfigObjectPropName = (propName) => {
12349
12558
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12350
12559
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12432,7 +12641,7 @@ const jsxNoNewObjectAsProp = defineRule({
12432
12641
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12433
12642
  context.report({
12434
12643
  node,
12435
- message: MESSAGE$36
12644
+ message: MESSAGE$47
12436
12645
  });
12437
12646
  }
12438
12647
  };
@@ -12440,7 +12649,7 @@ const jsxNoNewObjectAsProp = defineRule({
12440
12649
  });
12441
12650
  //#endregion
12442
12651
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
12443
- const MESSAGE$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12652
+ const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12444
12653
  const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
12445
12654
  const resolveSettings$28 = (settings) => {
12446
12655
  const reactDoctor = settings?.["react-doctor"];
@@ -12481,7 +12690,7 @@ const jsxNoScriptUrl = defineRule({
12481
12690
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12482
12691
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12483
12692
  node: attribute,
12484
- message: MESSAGE$35
12693
+ message: MESSAGE$46
12485
12694
  });
12486
12695
  }
12487
12696
  } };
@@ -12796,7 +13005,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12796
13005
  });
12797
13006
  //#endregion
12798
13007
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
12799
- const MESSAGE$34 = "You can't tell what props reach this element when you spread them.";
13008
+ const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
12800
13009
  const resolveSettings$25 = (settings) => {
12801
13010
  const reactDoctor = settings?.["react-doctor"];
12802
13011
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12837,7 +13046,7 @@ const jsxPropsNoSpreading = defineRule({
12837
13046
  }
12838
13047
  context.report({
12839
13048
  node: attribute,
12840
- message: MESSAGE$34
13049
+ message: MESSAGE$45
12841
13050
  });
12842
13051
  }
12843
13052
  } };
@@ -13065,7 +13274,7 @@ const labelHasAssociatedControl = defineRule({
13065
13274
  });
13066
13275
  //#endregion
13067
13276
  //#region src/plugin/rules/a11y/lang.ts
13068
- const MESSAGE$33 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13277
+ const MESSAGE$44 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13069
13278
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13070
13279
  "aa",
13071
13280
  "ab",
@@ -13277,7 +13486,7 @@ const lang = defineRule({
13277
13486
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13278
13487
  context.report({
13279
13488
  node: langAttr,
13280
- message: MESSAGE$33
13489
+ message: MESSAGE$44
13281
13490
  });
13282
13491
  return;
13283
13492
  }
@@ -13286,7 +13495,7 @@ const lang = defineRule({
13286
13495
  if (value === null) return;
13287
13496
  if (!isValidLangTag(value)) context.report({
13288
13497
  node: langAttr,
13289
- message: MESSAGE$33
13498
+ message: MESSAGE$44
13290
13499
  });
13291
13500
  } })
13292
13501
  });
@@ -13312,6 +13521,7 @@ const mcpToolCapabilityRisk = defineRule({
13312
13521
  shouldScan: (file) => isProductionSourcePath(file.relativePath),
13313
13522
  pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
13314
13523
  requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
13524
+ ignoreStringLiterals: true,
13315
13525
  message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
13316
13526
  })
13317
13527
  });
@@ -13330,7 +13540,7 @@ const mdxSsrExecutionRisk = defineRule({
13330
13540
  });
13331
13541
  //#endregion
13332
13542
  //#region src/plugin/rules/a11y/media-has-caption.ts
13333
- const MESSAGE$32 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13543
+ const MESSAGE$43 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13334
13544
  const DEFAULT_AUDIO = ["audio"];
13335
13545
  const DEFAULT_VIDEO = ["video"];
13336
13546
  const DEFAULT_TRACK = ["track"];
@@ -13371,7 +13581,7 @@ const mediaHasCaption = defineRule({
13371
13581
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13372
13582
  context.report({
13373
13583
  node: node.name,
13374
- message: MESSAGE$32
13584
+ message: MESSAGE$43
13375
13585
  });
13376
13586
  return;
13377
13587
  }
@@ -13388,7 +13598,7 @@ const mediaHasCaption = defineRule({
13388
13598
  return kindValue.value.toLowerCase() === "captions";
13389
13599
  })) context.report({
13390
13600
  node: node.name,
13391
- message: MESSAGE$32
13601
+ message: MESSAGE$43
13392
13602
  });
13393
13603
  } };
13394
13604
  }
@@ -15189,7 +15399,7 @@ const nextjsNoVercelOgImport = defineRule({
15189
15399
  });
15190
15400
  //#endregion
15191
15401
  //#region src/plugin/rules/a11y/no-access-key.ts
15192
- const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15402
+ const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15193
15403
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15194
15404
  const noAccessKey = defineRule({
15195
15405
  id: "no-access-key",
@@ -15206,7 +15416,7 @@ const noAccessKey = defineRule({
15206
15416
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15207
15417
  context.report({
15208
15418
  node: accessKey,
15209
- message: MESSAGE$31
15419
+ message: MESSAGE$42
15210
15420
  });
15211
15421
  return;
15212
15422
  }
@@ -15216,7 +15426,7 @@ const noAccessKey = defineRule({
15216
15426
  if (isUndefinedIdentifier(expression)) return;
15217
15427
  context.report({
15218
15428
  node: accessKey,
15219
- message: MESSAGE$31
15429
+ message: MESSAGE$42
15220
15430
  });
15221
15431
  }
15222
15432
  } })
@@ -15698,8 +15908,41 @@ const noAdjustStateOnPropChange = defineRule({
15698
15908
  } })
15699
15909
  });
15700
15910
  //#endregion
15911
+ //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
15912
+ const getStringFromClassNameAttr = (node) => {
15913
+ if (!isNodeOfType(node, "JSXOpeningElement")) return null;
15914
+ const classAttr = findJsxAttribute(node.attributes ?? [], "className");
15915
+ if (!classAttr?.value) return null;
15916
+ if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
15917
+ if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
15918
+ if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
15919
+ return null;
15920
+ };
15921
+ //#endregion
15922
+ //#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
15923
+ const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
15924
+ const noArbitraryPxFontSize = defineRule({
15925
+ id: "no-arbitrary-px-font-size",
15926
+ title: "Pixel arbitrary font size",
15927
+ tags: ["design", "test-noise"],
15928
+ severity: "warn",
15929
+ category: "Accessibility",
15930
+ recommendation: "Use `rem` for arbitrary font sizes (`text-[0.8125rem]`, not `text-[13px]`) so text scales with the user's root font-size preference. Pixels stay fine for `border-*` / `outline-*`.",
15931
+ create: (context) => ({ JSXOpeningElement(node) {
15932
+ const classNameValue = getStringFromClassNameAttr(node);
15933
+ if (!classNameValue) return;
15934
+ for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
15935
+ const rem = parseFloat(match[1]) / 16;
15936
+ context.report({
15937
+ node,
15938
+ message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
15939
+ });
15940
+ }
15941
+ } })
15942
+ });
15943
+ //#endregion
15701
15944
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
15702
- const MESSAGE$30 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15945
+ const MESSAGE$41 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15703
15946
  const noAriaHiddenOnFocusable = defineRule({
15704
15947
  id: "no-aria-hidden-on-focusable",
15705
15948
  title: "aria-hidden on focusable element",
@@ -15726,7 +15969,7 @@ const noAriaHiddenOnFocusable = defineRule({
15726
15969
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15727
15970
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15728
15971
  node: ariaHidden,
15729
- message: MESSAGE$30
15972
+ message: MESSAGE$41
15730
15973
  });
15731
15974
  } })
15732
15975
  });
@@ -16094,7 +16337,7 @@ const noArrayIndexAsKey = defineRule({
16094
16337
  });
16095
16338
  //#endregion
16096
16339
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
16097
- const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
16340
+ const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
16098
16341
  const SECOND_INDEX_METHODS = new Set([
16099
16342
  "every",
16100
16343
  "filter",
@@ -16298,7 +16541,7 @@ const noArrayIndexKey = defineRule({
16298
16541
  }
16299
16542
  context.report({
16300
16543
  node: keyAttribute,
16301
- message: MESSAGE$29
16544
+ message: MESSAGE$40
16302
16545
  });
16303
16546
  },
16304
16547
  CallExpression(node) {
@@ -16318,15 +16561,35 @@ const noArrayIndexKey = defineRule({
16318
16561
  if (propName !== "key") continue;
16319
16562
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16320
16563
  node: property,
16321
- message: MESSAGE$29
16564
+ message: MESSAGE$40
16322
16565
  });
16323
16566
  }
16324
16567
  }
16325
16568
  })
16326
16569
  });
16327
16570
  //#endregion
16571
+ //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16572
+ const MESSAGE$39 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
16573
+ const noAsyncEffectCallback = defineRule({
16574
+ id: "no-async-effect-callback",
16575
+ title: "Async effect callback",
16576
+ severity: "warn",
16577
+ recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
16578
+ create: (context) => ({ CallExpression(node) {
16579
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
16580
+ const callback = getEffectCallback(node);
16581
+ if (!callback) return;
16582
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
16583
+ if (!callback.async) return;
16584
+ context.report({
16585
+ node: callback,
16586
+ message: MESSAGE$39
16587
+ });
16588
+ } })
16589
+ });
16590
+ //#endregion
16328
16591
  //#region src/plugin/rules/a11y/no-autofocus.ts
16329
- const MESSAGE$28 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16592
+ const MESSAGE$38 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16330
16593
  const resolveSettings$21 = (settings) => {
16331
16594
  const reactDoctor = settings?.["react-doctor"];
16332
16595
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16382,12 +16645,45 @@ const noAutofocus = defineRule({
16382
16645
  }
16383
16646
  context.report({
16384
16647
  node: autoFocusAttribute,
16385
- message: MESSAGE$28
16648
+ message: MESSAGE$38
16386
16649
  });
16387
16650
  } };
16388
16651
  }
16389
16652
  });
16390
16653
  //#endregion
16654
+ //#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
16655
+ const MESSAGE$37 = "Autoplaying media with sound is hostile to your users (and browsers block it). Add `muted` (with `playsInline`) to the autoplaying `<video>` / `<audio>`, or drop `autoPlay`.";
16656
+ const resolveStaticBoolean = (attribute) => {
16657
+ const value = attribute.value;
16658
+ if (!value) return true;
16659
+ const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
16660
+ if (isNodeOfType(literal, "Literal")) {
16661
+ if (literal.value === true || literal.value === "true") return true;
16662
+ if (literal.value === false || literal.value === "false") return false;
16663
+ }
16664
+ return null;
16665
+ };
16666
+ const noAutoplayWithoutMuted = defineRule({
16667
+ id: "no-autoplay-without-muted",
16668
+ title: "Autoplaying media without muted",
16669
+ severity: "warn",
16670
+ recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
16671
+ create: (context) => ({ JSXOpeningElement(node) {
16672
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
16673
+ const tagName = node.name.name;
16674
+ if (tagName !== "video" && tagName !== "audio") return;
16675
+ if (hasJsxSpreadAttribute(node.attributes)) return;
16676
+ const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
16677
+ if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
16678
+ const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
16679
+ if (muted && resolveStaticBoolean(muted) !== false) return;
16680
+ context.report({
16681
+ node: node.name,
16682
+ message: MESSAGE$37
16683
+ });
16684
+ } })
16685
+ });
16686
+ //#endregion
16391
16687
  //#region src/plugin/utils/create-relative-import-source.ts
16392
16688
  const createRelativeImportSource = (filename, targetFilePath) => {
16393
16689
  const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
@@ -16632,6 +16928,109 @@ const noBarrelImport = defineRule({
16632
16928
  }
16633
16929
  });
16634
16930
  //#endregion
16931
+ //#region src/plugin/utils/function-contains-react-render-output.ts
16932
+ const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
16933
+ "FunctionDeclaration",
16934
+ "FunctionExpression",
16935
+ "ArrowFunctionExpression",
16936
+ "ClassDeclaration",
16937
+ "ClassExpression"
16938
+ ]);
16939
+ const isReactImport$1 = (symbol) => {
16940
+ let importDeclaration = symbol.declarationNode?.parent;
16941
+ while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
16942
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
16943
+ return importDeclaration.source.value === "react";
16944
+ };
16945
+ const getImportedName = (symbol) => {
16946
+ if (symbol.kind !== "import") return null;
16947
+ if (!isReactImport$1(symbol)) return null;
16948
+ return getImportedName$1(symbol.declarationNode) ?? null;
16949
+ };
16950
+ const isReactNamespaceImport = (symbol) => {
16951
+ if (symbol.kind !== "import") return false;
16952
+ if (!isReactImport$1(symbol)) return false;
16953
+ return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
16954
+ };
16955
+ const isReactCreateElementIdentifierCall = (callee, scopes) => {
16956
+ if (!isNodeOfType(callee, "Identifier")) return false;
16957
+ const symbol = scopes.symbolFor(callee);
16958
+ return Boolean(symbol && getImportedName(symbol) === "createElement");
16959
+ };
16960
+ const isReactCreateElementMemberCall = (callee, scopes) => {
16961
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
16962
+ if (callee.computed) return false;
16963
+ if (!isNodeOfType(callee.object, "Identifier")) return false;
16964
+ if (!isNodeOfType(callee.property, "Identifier")) return false;
16965
+ if (callee.property.name !== "createElement") return false;
16966
+ const symbol = scopes.symbolFor(callee.object);
16967
+ return Boolean(symbol && isReactNamespaceImport(symbol));
16968
+ };
16969
+ const isReactCreateElementCall = (node, scopes) => {
16970
+ if (!isNodeOfType(node, "CallExpression")) return false;
16971
+ return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
16972
+ };
16973
+ const containsRenderOutput = (node, rootNode, scopes) => {
16974
+ if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
16975
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
16976
+ if (isReactCreateElementCall(node, scopes)) return true;
16977
+ const nodeRecord = node;
16978
+ for (const key of Object.keys(nodeRecord)) {
16979
+ if (key === "parent") continue;
16980
+ const child = nodeRecord[key];
16981
+ if (Array.isArray(child)) {
16982
+ for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
16983
+ } else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
16984
+ }
16985
+ return false;
16986
+ };
16987
+ const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
16988
+ //#endregion
16989
+ //#region src/plugin/utils/is-component-declaration.ts
16990
+ const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
16991
+ //#endregion
16992
+ //#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
16993
+ const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
16994
+ const symbolIsLocalComponent = (symbol, context) => {
16995
+ const declaration = symbol.declarationNode;
16996
+ if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
16997
+ if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
16998
+ return false;
16999
+ };
17000
+ const noCallComponentAsFunction = defineRule({
17001
+ id: "no-call-component-as-function",
17002
+ title: "Component called as a function",
17003
+ severity: "warn",
17004
+ tags: ["test-noise"],
17005
+ recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
17006
+ create: (context) => {
17007
+ const renderedJsxNames = /* @__PURE__ */ new Set();
17008
+ const candidateCalls = [];
17009
+ return {
17010
+ JSXOpeningElement(node) {
17011
+ if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
17012
+ },
17013
+ CallExpression(node) {
17014
+ if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
17015
+ node,
17016
+ callee: node.callee,
17017
+ name: node.callee.name
17018
+ });
17019
+ },
17020
+ "Program:exit"() {
17021
+ for (const candidate of candidateCalls) {
17022
+ const symbol = context.scopes.symbolFor(candidate.callee);
17023
+ if (!symbol) continue;
17024
+ if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
17025
+ node: candidate.node,
17026
+ message: message(candidate.name)
17027
+ });
17028
+ }
17029
+ }
17030
+ };
17031
+ }
17032
+ });
17033
+ //#endregion
16635
17034
  //#region src/plugin/utils/is-setter-identifier.ts
16636
17035
  const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
16637
17036
  //#endregion
@@ -16783,7 +17182,7 @@ const noChainStateUpdates = defineRule({
16783
17182
  });
16784
17183
  //#endregion
16785
17184
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
16786
- const MESSAGE$27 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17185
+ const MESSAGE$36 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
16787
17186
  const noChildrenProp = defineRule({
16788
17187
  id: "no-children-prop",
16789
17188
  title: "Children passed as a prop",
@@ -16795,7 +17194,7 @@ const noChildrenProp = defineRule({
16795
17194
  if (node.name.name !== "children") return;
16796
17195
  context.report({
16797
17196
  node: node.name,
16798
- message: MESSAGE$27
17197
+ message: MESSAGE$36
16799
17198
  });
16800
17199
  },
16801
17200
  CallExpression(node) {
@@ -16808,7 +17207,7 @@ const noChildrenProp = defineRule({
16808
17207
  const propertyKey = property.key;
16809
17208
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
16810
17209
  node: propertyKey,
16811
- message: MESSAGE$27
17210
+ message: MESSAGE$36
16812
17211
  });
16813
17212
  }
16814
17213
  }
@@ -16816,7 +17215,7 @@ const noChildrenProp = defineRule({
16816
17215
  });
16817
17216
  //#endregion
16818
17217
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
16819
- const MESSAGE$26 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17218
+ const MESSAGE$35 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
16820
17219
  const noCloneElement = defineRule({
16821
17220
  id: "no-clone-element",
16822
17221
  title: "cloneElement makes child props fragile",
@@ -16829,7 +17228,7 @@ const noCloneElement = defineRule({
16829
17228
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
16830
17229
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
16831
17230
  node: callee,
16832
- message: MESSAGE$26
17231
+ message: MESSAGE$35
16833
17232
  });
16834
17233
  return;
16835
17234
  }
@@ -16842,7 +17241,7 @@ const noCloneElement = defineRule({
16842
17241
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
16843
17242
  context.report({
16844
17243
  node: callee,
16845
- message: MESSAGE$26
17244
+ message: MESSAGE$35
16846
17245
  });
16847
17246
  }
16848
17247
  } })
@@ -16891,7 +17290,7 @@ const enclosingComponentOrHookName = (node) => {
16891
17290
  };
16892
17291
  //#endregion
16893
17292
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
16894
- const MESSAGE$25 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17293
+ const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
16895
17294
  const CONTEXT_MODULES = [
16896
17295
  "react",
16897
17296
  "use-context-selector",
@@ -16927,7 +17326,32 @@ const noCreateContextInRender = defineRule({
16927
17326
  if (!componentOrHookName) return;
16928
17327
  context.report({
16929
17328
  node,
16930
- message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
17329
+ message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
17330
+ });
17331
+ } })
17332
+ });
17333
+ //#endregion
17334
+ //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17335
+ const MESSAGE$33 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
17336
+ const noCreateRefInFunctionComponent = defineRule({
17337
+ id: "no-create-ref-in-function-component",
17338
+ title: "createRef in function component",
17339
+ severity: "warn",
17340
+ recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
17341
+ create: (context) => ({ CallExpression(node) {
17342
+ if (!isReactFunctionCall(node, "createRef")) return;
17343
+ if (isNodeOfType(node.callee, "Identifier")) {
17344
+ const symbol = context.scopes.symbolFor(node.callee);
17345
+ if (symbol && symbol.kind !== "import") return;
17346
+ }
17347
+ const enclosingFunction = nearestEnclosingFunction(node);
17348
+ if (!enclosingFunction) return;
17349
+ const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
17350
+ if (!displayName) return;
17351
+ if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17352
+ context.report({
17353
+ node,
17354
+ message: MESSAGE$33
16931
17355
  });
16932
17356
  } })
16933
17357
  });
@@ -17067,7 +17491,7 @@ const noCreateStoreInRender = defineRule({
17067
17491
  });
17068
17492
  //#endregion
17069
17493
  //#region src/plugin/rules/react-builtins/no-danger.ts
17070
- const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17494
+ const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17071
17495
  const noDanger = defineRule({
17072
17496
  id: "no-danger",
17073
17497
  title: "Raw HTML injection can run unsafe markup",
@@ -17080,7 +17504,7 @@ const noDanger = defineRule({
17080
17504
  if (!propAttribute) return;
17081
17505
  context.report({
17082
17506
  node: propAttribute.name,
17083
- message: MESSAGE$24
17507
+ message: MESSAGE$32
17084
17508
  });
17085
17509
  },
17086
17510
  CallExpression(node) {
@@ -17092,7 +17516,7 @@ const noDanger = defineRule({
17092
17516
  const propertyKey = property.key;
17093
17517
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17094
17518
  node: propertyKey,
17095
- message: MESSAGE$24
17519
+ message: MESSAGE$32
17096
17520
  });
17097
17521
  }
17098
17522
  }
@@ -17100,7 +17524,7 @@ const noDanger = defineRule({
17100
17524
  });
17101
17525
  //#endregion
17102
17526
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
17103
- const MESSAGE$23 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17527
+ const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17104
17528
  const isLineBreak = (child) => {
17105
17529
  if (!isNodeOfType(child, "JSXText")) return false;
17106
17530
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17170,7 +17594,7 @@ const noDangerWithChildren = defineRule({
17170
17594
  if (!hasChildrenProp && !hasNestedChildren) return;
17171
17595
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17172
17596
  node: opening,
17173
- message: MESSAGE$23
17597
+ message: MESSAGE$31
17174
17598
  });
17175
17599
  },
17176
17600
  CallExpression(node) {
@@ -17182,7 +17606,7 @@ const noDangerWithChildren = defineRule({
17182
17606
  if (!propsShape.hasDangerously) return;
17183
17607
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17184
17608
  node,
17185
- message: MESSAGE$23
17609
+ message: MESSAGE$31
17186
17610
  });
17187
17611
  }
17188
17612
  })
@@ -17347,6 +17771,37 @@ const noDefaultProps = defineRule({
17347
17771
  } })
17348
17772
  });
17349
17773
  //#endregion
17774
+ //#region src/plugin/utils/get-class-name-tokens.ts
17775
+ const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
17776
+ //#endregion
17777
+ //#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
17778
+ const renameDeprecatedToken = (token) => {
17779
+ if (token === "overflow-ellipsis") return "text-ellipsis";
17780
+ if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
17781
+ if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
17782
+ if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
17783
+ return null;
17784
+ };
17785
+ const noDeprecatedTailwindClass = defineRule({
17786
+ id: "no-deprecated-tailwind-class",
17787
+ title: "Deprecated Tailwind v4 utility",
17788
+ tags: ["design", "test-noise"],
17789
+ severity: "warn",
17790
+ requires: ["tailwind:4"],
17791
+ recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
17792
+ create: (context) => ({ JSXOpeningElement(node) {
17793
+ const classNameValue = getStringFromClassNameAttr(node);
17794
+ if (!classNameValue) return;
17795
+ for (const token of getClassNameTokens(classNameValue)) {
17796
+ const replacement = renameDeprecatedToken(token);
17797
+ if (replacement) context.report({
17798
+ node,
17799
+ message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
17800
+ });
17801
+ }
17802
+ } })
17803
+ });
17804
+ //#endregion
17350
17805
  //#region src/plugin/utils/is-initial-only-prop-name.ts
17351
17806
  const isInitialOnlyPropName = (propName) => {
17352
17807
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -17759,7 +18214,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
17759
18214
  //#endregion
17760
18215
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
17761
18216
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
17762
- const MESSAGE$22 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18217
+ const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
17763
18218
  const resolveSettings$20 = (settings) => {
17764
18219
  const reactDoctor = settings?.["react-doctor"];
17765
18220
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -17778,7 +18233,7 @@ const noDidMountSetState = defineRule({
17778
18233
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17779
18234
  context.report({
17780
18235
  node: node.callee,
17781
- message: MESSAGE$22
18236
+ message: MESSAGE$30
17782
18237
  });
17783
18238
  } };
17784
18239
  }
@@ -17786,7 +18241,7 @@ const noDidMountSetState = defineRule({
17786
18241
  //#endregion
17787
18242
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
17788
18243
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
17789
- const MESSAGE$21 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18244
+ const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
17790
18245
  const resolveSettings$19 = (settings) => {
17791
18246
  const reactDoctor = settings?.["react-doctor"];
17792
18247
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -17805,7 +18260,7 @@ const noDidUpdateSetState = defineRule({
17805
18260
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17806
18261
  context.report({
17807
18262
  node: node.callee,
17808
- message: MESSAGE$21
18263
+ message: MESSAGE$29
17809
18264
  });
17810
18265
  } };
17811
18266
  }
@@ -17828,7 +18283,7 @@ const isStateMemberExpression = (node) => {
17828
18283
  };
17829
18284
  //#endregion
17830
18285
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
17831
- const MESSAGE$20 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18286
+ const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
17832
18287
  const shouldIgnoreMutation = (node) => {
17833
18288
  let isConstructor = false;
17834
18289
  let isInsideCallExpression = false;
@@ -17850,7 +18305,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
17850
18305
  if (shouldIgnoreMutation(reportNode)) return;
17851
18306
  context.report({
17852
18307
  node: reportNode,
17853
- message: MESSAGE$20
18308
+ message: MESSAGE$28
17854
18309
  });
17855
18310
  };
17856
18311
  const noDirectMutationState = defineRule({
@@ -18060,6 +18515,26 @@ const noDocumentStartViewTransition = defineRule({
18060
18515
  } })
18061
18516
  });
18062
18517
  //#endregion
18518
+ //#region src/plugin/rules/js-performance/no-document-write.ts
18519
+ const MESSAGE$27 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
18520
+ const WRITE_METHODS = new Set(["write", "writeln"]);
18521
+ const noDocumentWrite = defineRule({
18522
+ id: "no-document-write",
18523
+ title: "document.write/writeln",
18524
+ severity: "warn",
18525
+ recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
18526
+ create: (context) => ({ CallExpression(node) {
18527
+ const callee = node.callee;
18528
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
18529
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
18530
+ if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18531
+ context.report({
18532
+ node,
18533
+ message: MESSAGE$27
18534
+ });
18535
+ } })
18536
+ });
18537
+ //#endregion
18063
18538
  //#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
18064
18539
  const noDynamicImportPath = defineRule({
18065
18540
  id: "no-dynamic-import-path",
@@ -19438,7 +19913,7 @@ const ALLOWED_NAMESPACES = new Set([
19438
19913
  "ReactDOM",
19439
19914
  "ReactDom"
19440
19915
  ]);
19441
- const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19916
+ const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19442
19917
  const noFindDomNode = defineRule({
19443
19918
  id: "no-find-dom-node",
19444
19919
  title: "findDOMNode breaks component encapsulation",
@@ -19449,7 +19924,7 @@ const noFindDomNode = defineRule({
19449
19924
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19450
19925
  context.report({
19451
19926
  node: callee,
19452
- message: MESSAGE$19
19927
+ message: MESSAGE$26
19453
19928
  });
19454
19929
  return;
19455
19930
  }
@@ -19460,7 +19935,7 @@ const noFindDomNode = defineRule({
19460
19935
  if (callee.property.name !== "findDOMNode") return;
19461
19936
  context.report({
19462
19937
  node: callee.property,
19463
- message: MESSAGE$19
19938
+ message: MESSAGE$26
19464
19939
  });
19465
19940
  }
19466
19941
  } })
@@ -19501,6 +19976,41 @@ const noFullLodashImport = defineRule({
19501
19976
  } })
19502
19977
  });
19503
19978
  //#endregion
19979
+ //#region src/plugin/rules/design/no-full-viewport-width.ts
19980
+ const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
19981
+ const WIDTH_KEYS = new Set(["width", "minWidth"]);
19982
+ const MESSAGE$25 = "`100vw` is wider than the viewport whenever a scrollbar is visible, so it triggers horizontal scroll on most desktops. Use `w-full` / `width: 100%` (with the parent's padding) for a full-bleed element.";
19983
+ const noFullViewportWidth = defineRule({
19984
+ id: "no-full-viewport-width",
19985
+ title: "Full viewport width causes overflow",
19986
+ tags: ["design", "test-noise"],
19987
+ severity: "warn",
19988
+ recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
19989
+ create: (context) => ({
19990
+ JSXAttribute(node) {
19991
+ const expression = getInlineStyleExpression(node);
19992
+ if (!expression) return;
19993
+ for (const property of expression.properties ?? []) {
19994
+ const key = getStylePropertyKey(property);
19995
+ if (!key || !WIDTH_KEYS.has(key)) continue;
19996
+ const value = getStylePropertyStringValue(property);
19997
+ if (value && value.trim().toLowerCase() === "100vw") context.report({
19998
+ node: property,
19999
+ message: MESSAGE$25
20000
+ });
20001
+ }
20002
+ },
20003
+ JSXOpeningElement(node) {
20004
+ const classNameValue = getStringFromClassNameAttr(node);
20005
+ if (!classNameValue) return;
20006
+ if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
20007
+ node,
20008
+ message: MESSAGE$25
20009
+ });
20010
+ }
20011
+ })
20012
+ });
20013
+ //#endregion
19504
20014
  //#region src/plugin/rules/architecture/no-generic-handler-names.ts
19505
20015
  const noGenericHandlerNames = defineRule({
19506
20016
  id: "no-generic-handler-names",
@@ -19523,64 +20033,6 @@ const noGenericHandlerNames = defineRule({
19523
20033
  } })
19524
20034
  });
19525
20035
  //#endregion
19526
- //#region src/plugin/utils/function-contains-react-render-output.ts
19527
- const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
19528
- "FunctionDeclaration",
19529
- "FunctionExpression",
19530
- "ArrowFunctionExpression",
19531
- "ClassDeclaration",
19532
- "ClassExpression"
19533
- ]);
19534
- const isReactImport$1 = (symbol) => {
19535
- let importDeclaration = symbol.declarationNode?.parent;
19536
- while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
19537
- if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
19538
- return importDeclaration.source.value === "react";
19539
- };
19540
- const getImportedName = (symbol) => {
19541
- if (symbol.kind !== "import") return null;
19542
- if (!isReactImport$1(symbol)) return null;
19543
- return getImportedName$1(symbol.declarationNode) ?? null;
19544
- };
19545
- const isReactNamespaceImport = (symbol) => {
19546
- if (symbol.kind !== "import") return false;
19547
- if (!isReactImport$1(symbol)) return false;
19548
- return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
19549
- };
19550
- const isReactCreateElementIdentifierCall = (callee, scopes) => {
19551
- if (!isNodeOfType(callee, "Identifier")) return false;
19552
- const symbol = scopes.symbolFor(callee);
19553
- return Boolean(symbol && getImportedName(symbol) === "createElement");
19554
- };
19555
- const isReactCreateElementMemberCall = (callee, scopes) => {
19556
- if (!isNodeOfType(callee, "MemberExpression")) return false;
19557
- if (callee.computed) return false;
19558
- if (!isNodeOfType(callee.object, "Identifier")) return false;
19559
- if (!isNodeOfType(callee.property, "Identifier")) return false;
19560
- if (callee.property.name !== "createElement") return false;
19561
- const symbol = scopes.symbolFor(callee.object);
19562
- return Boolean(symbol && isReactNamespaceImport(symbol));
19563
- };
19564
- const isReactCreateElementCall = (node, scopes) => {
19565
- if (!isNodeOfType(node, "CallExpression")) return false;
19566
- return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
19567
- };
19568
- const containsRenderOutput = (node, rootNode, scopes) => {
19569
- if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
19570
- if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
19571
- if (isReactCreateElementCall(node, scopes)) return true;
19572
- const nodeRecord = node;
19573
- for (const key of Object.keys(nodeRecord)) {
19574
- if (key === "parent") continue;
19575
- const child = nodeRecord[key];
19576
- if (Array.isArray(child)) {
19577
- for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
19578
- } else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
19579
- }
19580
- return false;
19581
- };
19582
- const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
19583
- //#endregion
19584
20036
  //#region src/plugin/rules/architecture/no-giant-component.ts
19585
20037
  const noGiantComponent = defineRule({
19586
20038
  id: "no-giant-component",
@@ -19621,7 +20073,7 @@ const noGiantComponent = defineRule({
19621
20073
  });
19622
20074
  //#endregion
19623
20075
  //#region src/plugin/constants/style.ts
19624
- const LAYOUT_PROPERTIES = new Set([
20076
+ const LAYOUT_PROPERTIES$1 = new Set([
19625
20077
  "width",
19626
20078
  "height",
19627
20079
  "top",
@@ -19691,17 +20143,6 @@ const noGlobalCssVariableAnimation = defineRule({
19691
20143
  } })
19692
20144
  });
19693
20145
  //#endregion
19694
- //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
19695
- const getStringFromClassNameAttr = (node) => {
19696
- if (!isNodeOfType(node, "JSXOpeningElement")) return null;
19697
- const classAttr = findJsxAttribute(node.attributes ?? [], "className");
19698
- if (!classAttr?.value) return null;
19699
- if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
19700
- if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
19701
- 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;
19702
- return null;
19703
- };
19704
- //#endregion
19705
20146
  //#region src/plugin/rules/design/no-gradient-text.ts
19706
20147
  const noGradientText = defineRule({
19707
20148
  id: "no-gradient-text",
@@ -19759,6 +20200,26 @@ const noGrayOnColoredBackground = defineRule({
19759
20200
  } })
19760
20201
  });
19761
20202
  //#endregion
20203
+ //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
20204
+ const MESSAGE$24 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
20205
+ const noImgLazyWithHighFetchpriority = defineRule({
20206
+ id: "no-img-lazy-with-high-fetchpriority",
20207
+ title: "Lazy image with high fetchPriority",
20208
+ severity: "warn",
20209
+ recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
20210
+ create: (context) => ({ JSXOpeningElement(node) {
20211
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
20212
+ const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
20213
+ if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
20214
+ const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
20215
+ if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20216
+ context.report({
20217
+ node: node.name,
20218
+ message: MESSAGE$24
20219
+ });
20220
+ } })
20221
+ });
20222
+ //#endregion
19762
20223
  //#region src/plugin/rules/state-and-effects/no-initialize-state.ts
19763
20224
  const noInitializeState = defineRule({
19764
20225
  id: "no-initialize-state",
@@ -19988,8 +20449,31 @@ const noIsMounted = defineRule({
19988
20449
  } })
19989
20450
  });
19990
20451
  //#endregion
20452
+ //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20453
+ const MESSAGE$23 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
20454
+ const isJsonMethodCall = (node, method) => {
20455
+ if (!isNodeOfType(node, "CallExpression")) return false;
20456
+ const callee = node.callee;
20457
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
20458
+ };
20459
+ const noJsonParseStringifyClone = defineRule({
20460
+ id: "no-json-parse-stringify-clone",
20461
+ title: "JSON parse/stringify deep clone",
20462
+ severity: "warn",
20463
+ recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
20464
+ create: (context) => ({ CallExpression(node) {
20465
+ if (!isJsonMethodCall(node, "parse")) return;
20466
+ const firstArgument = node.arguments?.[0];
20467
+ if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20468
+ context.report({
20469
+ node,
20470
+ message: MESSAGE$23
20471
+ });
20472
+ } })
20473
+ });
20474
+ //#endregion
19991
20475
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
19992
- const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20476
+ const MESSAGE$22 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
19993
20477
  const isJsxElementTypeReference = (node) => {
19994
20478
  if (!isNodeOfType(node, "TSTypeReference")) return false;
19995
20479
  const typeName = node.typeName;
@@ -20006,7 +20490,7 @@ const checkReturnType = (context, returnType) => {
20006
20490
  if (!typeAnnotation) return;
20007
20491
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20008
20492
  node: typeAnnotation,
20009
- message: MESSAGE$18
20493
+ message: MESSAGE$22
20010
20494
  });
20011
20495
  };
20012
20496
  const noJsxElementType = defineRule({
@@ -20116,7 +20600,7 @@ const noLayoutPropertyAnimation = defineRule({
20116
20600
  let propertyName = null;
20117
20601
  if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
20118
20602
  else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
20119
- if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
20603
+ if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
20120
20604
  node: property,
20121
20605
  message: `This stutters because animating "${propertyName}" makes the browser redo page layout every frame, so animate transform or scale instead, or use the layout prop`
20122
20606
  });
@@ -20306,13 +20790,138 @@ const noLongTransitionDuration = defineRule({
20306
20790
  } })
20307
20791
  });
20308
20792
  //#endregion
20793
+ //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
20794
+ const getStylePropertyNumberValue = (property) => {
20795
+ if (!isNodeOfType(property, "Property")) return null;
20796
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
20797
+ if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
20798
+ return null;
20799
+ };
20800
+ //#endregion
20801
+ //#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
20802
+ const linearizeChannel = (channel) => {
20803
+ const normalized = channel / 255;
20804
+ return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
20805
+ };
20806
+ const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
20807
+ const getWcagContrastRatio = (foreground, background) => {
20808
+ const foregroundLuminance = relativeLuminance(foreground);
20809
+ const backgroundLuminance = relativeLuminance(background);
20810
+ const lighter = Math.max(foregroundLuminance, backgroundLuminance);
20811
+ const darker = Math.min(foregroundLuminance, backgroundLuminance);
20812
+ return (lighter + .05) / (darker + .05);
20813
+ };
20814
+ //#endregion
20815
+ //#region src/plugin/rules/design/no-low-contrast-inline-style.ts
20816
+ const UNRESOLVABLE = new Set([
20817
+ "transparent",
20818
+ "currentcolor",
20819
+ "inherit",
20820
+ "initial",
20821
+ "unset",
20822
+ "revert",
20823
+ "none"
20824
+ ]);
20825
+ const resolveOpaqueColor = (raw) => {
20826
+ const value = raw.trim().toLowerCase();
20827
+ if (UNRESOLVABLE.has(value)) return null;
20828
+ if (value === "white") return {
20829
+ red: 255,
20830
+ green: 255,
20831
+ blue: 255
20832
+ };
20833
+ if (value === "black") return {
20834
+ red: 0,
20835
+ green: 0,
20836
+ blue: 0
20837
+ };
20838
+ if (value.startsWith("var(")) return null;
20839
+ if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
20840
+ if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
20841
+ if (value.startsWith("rgb")) {
20842
+ const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
20843
+ if (inner.includes("/") || inner.split(",").length >= 4) return null;
20844
+ }
20845
+ return parseColorToRgb(value);
20846
+ };
20847
+ const toPx = (property) => {
20848
+ const numberValue = getStylePropertyNumberValue(property);
20849
+ if (numberValue !== null) return numberValue;
20850
+ const stringValue = getStylePropertyStringValue(property);
20851
+ if (stringValue === null) return null;
20852
+ const pxMatch = stringValue.match(/^([\d.]+)px$/);
20853
+ if (pxMatch) return parseFloat(pxMatch[1]);
20854
+ const remMatch = stringValue.match(/^([\d.]+)rem$/);
20855
+ if (remMatch) return parseFloat(remMatch[1]) * 16;
20856
+ return null;
20857
+ };
20858
+ const isBoldWeight = (property) => {
20859
+ const numberValue = getStylePropertyNumberValue(property);
20860
+ if (numberValue !== null) return numberValue >= 700;
20861
+ const stringValue = getStylePropertyStringValue(property);
20862
+ if (stringValue === null) return false;
20863
+ if (stringValue === "bold" || stringValue === "bolder") return true;
20864
+ const numericWeight = Number(stringValue);
20865
+ return Number.isFinite(numericWeight) && numericWeight >= 700;
20866
+ };
20867
+ const noLowContrastInlineStyle = defineRule({
20868
+ id: "no-low-contrast-inline-style",
20869
+ title: "Low-contrast text in inline style",
20870
+ tags: ["test-noise"],
20871
+ severity: "warn",
20872
+ category: "Accessibility",
20873
+ recommendation: "Text needs a WCAG contrast ratio of at least 4.5:1 (3:1 for large/bold text) against its background. Darken or lighten one of the colors until it passes.",
20874
+ create: (context) => ({ JSXAttribute(node) {
20875
+ const expression = getInlineStyleExpression(node);
20876
+ if (!expression) return;
20877
+ const properties = expression.properties ?? [];
20878
+ if (properties.some((property) => property.type === "SpreadElement")) return;
20879
+ let foreground = null;
20880
+ let backgroundColorRaw = null;
20881
+ let backgroundShorthandRaw = null;
20882
+ let backgroundIsUnknown = false;
20883
+ let fontSizePx = null;
20884
+ let isBold = false;
20885
+ for (const property of properties) {
20886
+ const key = getStylePropertyKey(property);
20887
+ if (!key) continue;
20888
+ if (key === "backgroundImage") {
20889
+ backgroundIsUnknown = true;
20890
+ continue;
20891
+ }
20892
+ if (key === "fontSize" && property.type === "Property") {
20893
+ fontSizePx = toPx(property);
20894
+ continue;
20895
+ }
20896
+ if (key === "fontWeight" && property.type === "Property") {
20897
+ isBold = isBoldWeight(property);
20898
+ continue;
20899
+ }
20900
+ const stringValue = getStylePropertyStringValue(property);
20901
+ if (key === "color") {
20902
+ if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
20903
+ } else if (key === "backgroundColor") backgroundColorRaw = stringValue;
20904
+ else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
20905
+ else backgroundShorthandRaw = stringValue;
20906
+ }
20907
+ if (backgroundIsUnknown) return;
20908
+ if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
20909
+ const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
20910
+ const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
20911
+ if (!foreground || !background) return;
20912
+ const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
20913
+ const ratio = getWcagContrastRatio(foreground, background);
20914
+ if (ratio < threshold) context.report({
20915
+ node,
20916
+ message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
20917
+ });
20918
+ } })
20919
+ });
20920
+ //#endregion
20309
20921
  //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
20310
20922
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20311
20923
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
20312
20924
  //#endregion
20313
- //#region src/plugin/utils/is-component-declaration.ts
20314
- const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
20315
- //#endregion
20316
20925
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
20317
20926
  const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
20318
20927
  const found = /* @__PURE__ */ new Set();
@@ -20464,7 +21073,7 @@ const noMoment = defineRule({
20464
21073
  });
20465
21074
  //#endregion
20466
21075
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
20467
- const MESSAGE$17 = "This file declares several components, so each component is harder to find, test, and change.";
21076
+ const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
20468
21077
  const resolveSettings$16 = (settings) => {
20469
21078
  const reactDoctor = settings?.["react-doctor"];
20470
21079
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -20786,7 +21395,7 @@ const noMultiComp = defineRule({
20786
21395
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
20787
21396
  for (const component of flagged.slice(1)) context.report({
20788
21397
  node: component.reportNode,
20789
- message: MESSAGE$17
21398
+ message: MESSAGE$21
20790
21399
  });
20791
21400
  } };
20792
21401
  }
@@ -20954,7 +21563,7 @@ const resolveReducerFunction = (node, currentFilename) => {
20954
21563
  };
20955
21564
  //#endregion
20956
21565
  //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
20957
- const MESSAGE$16 = "This reducer changes state in place, so your update is silently skipped.";
21566
+ const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
20958
21567
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
20959
21568
  "copyWithin",
20960
21569
  "fill",
@@ -21164,7 +21773,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21164
21773
  reportedNodes.add(options.crossFileConsumerCallSite);
21165
21774
  context.report({
21166
21775
  node: options.crossFileConsumerCallSite,
21167
- message: `${MESSAGE$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21776
+ message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21168
21777
  });
21169
21778
  return;
21170
21779
  }
@@ -21173,7 +21782,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21173
21782
  reportedNodes.add(mutation.node);
21174
21783
  context.report({
21175
21784
  node: mutation.node,
21176
- message: MESSAGE$16
21785
+ message: MESSAGE$20
21177
21786
  });
21178
21787
  }
21179
21788
  };
@@ -21445,7 +22054,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21445
22054
  });
21446
22055
  //#endregion
21447
22056
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
21448
- const MESSAGE$15 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
22057
+ const MESSAGE$19 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21449
22058
  const resolveSettings$14 = (settings) => {
21450
22059
  const reactDoctor = settings?.["react-doctor"];
21451
22060
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -21473,7 +22082,7 @@ const noNoninteractiveTabindex = defineRule({
21473
22082
  if (numeric === null) {
21474
22083
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
21475
22084
  node: tabIndex,
21476
- message: MESSAGE$15
22085
+ message: MESSAGE$19
21477
22086
  });
21478
22087
  return;
21479
22088
  }
@@ -21486,7 +22095,7 @@ const noNoninteractiveTabindex = defineRule({
21486
22095
  if (!roleAttribute) {
21487
22096
  context.report({
21488
22097
  node: tabIndex,
21489
- message: MESSAGE$15
22098
+ message: MESSAGE$19
21490
22099
  });
21491
22100
  return;
21492
22101
  }
@@ -21500,20 +22109,12 @@ const noNoninteractiveTabindex = defineRule({
21500
22109
  }
21501
22110
  context.report({
21502
22111
  node: tabIndex,
21503
- message: MESSAGE$15
22112
+ message: MESSAGE$19
21504
22113
  });
21505
22114
  } };
21506
22115
  }
21507
22116
  });
21508
22117
  //#endregion
21509
- //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
21510
- const getStylePropertyNumberValue = (property) => {
21511
- if (!isNodeOfType(property, "Property")) return null;
21512
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
21513
- if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
21514
- return null;
21515
- };
21516
- //#endregion
21517
22118
  //#region src/plugin/rules/design/no-outline-none.ts
21518
22119
  const noOutlineNone = defineRule({
21519
22120
  id: "no-outline-none",
@@ -22191,7 +22792,7 @@ const noRandomKey = defineRule({
22191
22792
  });
22192
22793
  //#endregion
22193
22794
  //#region src/plugin/rules/react-builtins/no-react-children.ts
22194
- const MESSAGE$14 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22795
+ const MESSAGE$18 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22195
22796
  const isChildrenIdentifier = (node, contextNode) => {
22196
22797
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22197
22798
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22217,13 +22818,13 @@ const noReactChildren = defineRule({
22217
22818
  if (isChildrenIdentifier(memberObject, node)) {
22218
22819
  context.report({
22219
22820
  node: calleeOuter,
22220
- message: MESSAGE$14
22821
+ message: MESSAGE$18
22221
22822
  });
22222
22823
  return;
22223
22824
  }
22224
22825
  if (isReactNamespaceMember(memberObject, node)) context.report({
22225
22826
  node: calleeOuter,
22226
- message: MESSAGE$14
22827
+ message: MESSAGE$18
22227
22828
  });
22228
22829
  } })
22229
22830
  });
@@ -22334,6 +22935,86 @@ const noReact19DeprecatedApis = defineRule({
22334
22935
  })
22335
22936
  });
22336
22937
  //#endregion
22938
+ //#region src/plugin/rules/design/no-redundant-display-class.ts
22939
+ const BLOCK_DEFAULT_TAGS = new Set([
22940
+ "div",
22941
+ "p",
22942
+ "section",
22943
+ "article",
22944
+ "main",
22945
+ "header",
22946
+ "footer",
22947
+ "nav",
22948
+ "aside",
22949
+ "figure",
22950
+ "figcaption",
22951
+ "blockquote",
22952
+ "form",
22953
+ "fieldset",
22954
+ "address",
22955
+ "pre",
22956
+ "ul",
22957
+ "ol",
22958
+ "dl",
22959
+ "dt",
22960
+ "dd",
22961
+ "h1",
22962
+ "h2",
22963
+ "h3",
22964
+ "h4",
22965
+ "h5",
22966
+ "h6"
22967
+ ]);
22968
+ const INLINE_DEFAULT_TAGS = new Set([
22969
+ "span",
22970
+ "a",
22971
+ "b",
22972
+ "i",
22973
+ "em",
22974
+ "strong",
22975
+ "small",
22976
+ "code",
22977
+ "abbr",
22978
+ "cite",
22979
+ "label",
22980
+ "mark",
22981
+ "q",
22982
+ "s",
22983
+ "u",
22984
+ "sub",
22985
+ "sup",
22986
+ "kbd",
22987
+ "samp",
22988
+ "var",
22989
+ "time"
22990
+ ]);
22991
+ const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
22992
+ const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
22993
+ const noRedundantDisplayClass = defineRule({
22994
+ id: "no-redundant-display-class",
22995
+ title: "Redundant display utility",
22996
+ tags: ["design", "test-noise"],
22997
+ severity: "warn",
22998
+ recommendation: "Drop the display class that matches the element's default (`block` on a `<div>`, `inline` on a `<span>`). It is pure noise; keep only display changes like `flex`, `grid`, or `hidden`.",
22999
+ create: (context) => ({ JSXOpeningElement(node) {
23000
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
23001
+ const tagName = node.name.name;
23002
+ const classNameValue = getStringFromClassNameAttr(node);
23003
+ if (!classNameValue) return;
23004
+ if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
23005
+ context.report({
23006
+ node,
23007
+ message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
23008
+ });
23009
+ return;
23010
+ }
23011
+ if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
23012
+ node,
23013
+ message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
23014
+ });
23015
+ } })
23016
+ });
23017
+ //#endregion
22337
23018
  //#region src/plugin/constants/aria-element-roles.ts
22338
23019
  const ELEMENT_ROLE_PAIRS = [
22339
23020
  ["a", "link"],
@@ -22546,7 +23227,7 @@ const noRenderPropChildren = defineRule({
22546
23227
  });
22547
23228
  //#endregion
22548
23229
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
22549
- const MESSAGE$13 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
23230
+ const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22550
23231
  const isReactDomRenderCall = (node) => {
22551
23232
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
22552
23233
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -22570,7 +23251,7 @@ const noRenderReturnValue = defineRule({
22570
23251
  if (!isUsedAsReturnValue(node.parent)) return;
22571
23252
  context.report({
22572
23253
  node: node.callee,
22573
- message: MESSAGE$13
23254
+ message: MESSAGE$17
22574
23255
  });
22575
23256
  } })
22576
23257
  });
@@ -22730,11 +23411,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
22730
23411
  return "unknown";
22731
23412
  };
22732
23413
  //#endregion
22733
- //#region src/plugin/utils/get-identifier-trailing-word.ts
22734
- const getIdentifierTrailingWord = (identifierName) => {
22735
- return identifierName.match(/[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g)?.at(-1)?.toLowerCase() ?? identifierName.toLowerCase();
23414
+ //#region src/plugin/utils/tokenize-identifier-words.ts
23415
+ const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
23416
+ const tokenizeIdentifierWords = (identifierName) => {
23417
+ const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
23418
+ if (!words) return [];
23419
+ return words.map((word) => word.toLowerCase());
22736
23420
  };
22737
23421
  //#endregion
23422
+ //#region src/plugin/utils/get-identifier-trailing-word.ts
23423
+ const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
23424
+ //#endregion
22738
23425
  //#region src/plugin/constants/tanstack.ts
22739
23426
  const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
22740
23427
  const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
@@ -23262,7 +23949,7 @@ const getParentComponent = (node) => {
23262
23949
  };
23263
23950
  //#endregion
23264
23951
  //#region src/plugin/rules/react-builtins/no-set-state.ts
23265
- const MESSAGE$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23952
+ const MESSAGE$16 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23266
23953
  const noSetState = defineRule({
23267
23954
  id: "no-set-state",
23268
23955
  title: "Local class state forbidden",
@@ -23277,7 +23964,7 @@ const noSetState = defineRule({
23277
23964
  if (!getParentComponent(node)) return;
23278
23965
  context.report({
23279
23966
  node: node.callee,
23280
- message: MESSAGE$12
23967
+ message: MESSAGE$16
23281
23968
  });
23282
23969
  } })
23283
23970
  });
@@ -23439,7 +24126,7 @@ const isAbstractRole = (openingElement, settings) => {
23439
24126
  };
23440
24127
  //#endregion
23441
24128
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
23442
- const MESSAGE$11 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
24129
+ const MESSAGE$15 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
23443
24130
  const DEFAULT_HANDLERS = [
23444
24131
  "onClick",
23445
24132
  "onMouseDown",
@@ -23499,7 +24186,7 @@ const noStaticElementInteractions = defineRule({
23499
24186
  if (!roleAttribute || !roleAttribute.value) {
23500
24187
  context.report({
23501
24188
  node: node.name,
23502
- message: MESSAGE$11
24189
+ message: MESSAGE$15
23503
24190
  });
23504
24191
  return;
23505
24192
  }
@@ -23509,19 +24196,66 @@ const noStaticElementInteractions = defineRule({
23509
24196
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
23510
24197
  context.report({
23511
24198
  node: node.name,
23512
- message: MESSAGE$11
24199
+ message: MESSAGE$15
23513
24200
  });
23514
24201
  return;
23515
24202
  }
23516
24203
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
23517
24204
  context.report({
23518
24205
  node: node.name,
23519
- message: MESSAGE$11
24206
+ message: MESSAGE$15
23520
24207
  });
23521
24208
  } };
23522
24209
  }
23523
24210
  });
23524
24211
  //#endregion
24212
+ //#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
24213
+ const BOOLEAN_ATTRIBUTES = new Set([
24214
+ "disabled",
24215
+ "checked",
24216
+ "readonly",
24217
+ "required",
24218
+ "selected",
24219
+ "multiple",
24220
+ "autofocus",
24221
+ "autoplay",
24222
+ "controls",
24223
+ "loop",
24224
+ "muted",
24225
+ "open",
24226
+ "reversed",
24227
+ "default",
24228
+ "novalidate",
24229
+ "formnovalidate",
24230
+ "playsinline",
24231
+ "itemscope",
24232
+ "allowfullscreen"
24233
+ ]);
24234
+ const noStringFalseOnBooleanAttribute = defineRule({
24235
+ id: "no-string-false-on-boolean-attribute",
24236
+ title: "String true/false on a boolean attribute",
24237
+ severity: "warn",
24238
+ recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
24239
+ create: (context) => ({ JSXOpeningElement(node) {
24240
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
24241
+ const firstCharacter = node.name.name.charCodeAt(0);
24242
+ if (firstCharacter < 97 || firstCharacter > 122) return;
24243
+ for (const attribute of node.attributes) {
24244
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
24245
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
24246
+ if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
24247
+ const value = getJsxPropStringValue(attribute);
24248
+ if (value !== "false" && value !== "true") continue;
24249
+ const attributeName = attribute.name.name;
24250
+ const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
24251
+ context.report({
24252
+ node: attribute,
24253
+ message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
24254
+ });
24255
+ }
24256
+ } })
24257
+ });
24258
+ //#endregion
23525
24259
  //#region src/plugin/rules/react-builtins/no-string-refs.ts
23526
24260
  const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
23527
24261
  const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
@@ -23572,8 +24306,154 @@ const noStringRefs = defineRule({
23572
24306
  }
23573
24307
  });
23574
24308
  //#endregion
24309
+ //#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
24310
+ const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
24311
+ if (token.includes(":")) return false;
24312
+ if (!token.startsWith(prefix)) return false;
24313
+ const value = token.slice(prefix.length);
24314
+ if (value === "" || value === "current") return false;
24315
+ if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
24316
+ return true;
24317
+ });
24318
+ const isCurrentColor = (attribute) => {
24319
+ const value = getJsxPropStringValue(attribute);
24320
+ return value !== null && value.trim().toLowerCase() === "currentcolor";
24321
+ };
24322
+ const noSvgCurrentcolorWithFillClass = defineRule({
24323
+ id: "no-svg-currentcolor-with-fill-class",
24324
+ title: "currentColor fights a fill/stroke class",
24325
+ tags: ["design", "test-noise"],
24326
+ severity: "warn",
24327
+ recommendation: "Pick one source of truth: drop the `fill=\"currentColor\"` attribute and keep the `fill-*` class, or use `fill-current` to inherit the text color. Having both means the class silently wins.",
24328
+ create: (context) => ({ JSXOpeningElement(node) {
24329
+ const classNameValue = getStringFromClassNameAttr(node);
24330
+ if (!classNameValue) return;
24331
+ for (const paint of ["fill", "stroke"]) {
24332
+ const attribute = findJsxAttribute(node.attributes, paint);
24333
+ if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
24334
+ context.report({
24335
+ node: attribute,
24336
+ message: `\`${paint}="currentColor"\` and a \`${paint}-*\` color class on the same element conflict — the class wins. Remove one, or use \`${paint}-current\` to inherit the text color.`
24337
+ });
24338
+ return;
24339
+ }
24340
+ }
24341
+ } })
24342
+ });
24343
+ //#endregion
24344
+ //#region src/plugin/rules/js-performance/no-sync-xhr.ts
24345
+ const MESSAGE$14 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
24346
+ const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
24347
+ const noSyncXhr = defineRule({
24348
+ id: "no-sync-xhr",
24349
+ title: "Synchronous XMLHttpRequest",
24350
+ severity: "warn",
24351
+ recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
24352
+ create: (context) => ({ CallExpression(node) {
24353
+ const callee = node.callee;
24354
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
24355
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
24356
+ const asyncArgument = node.arguments?.[2];
24357
+ if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
24358
+ context.report({
24359
+ node,
24360
+ message: MESSAGE$14
24361
+ });
24362
+ } })
24363
+ });
24364
+ //#endregion
24365
+ //#region src/plugin/rules/design/no-tailwind-layout-transition.ts
24366
+ const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
24367
+ const LAYOUT_PROPERTIES = new Set([
24368
+ "width",
24369
+ "height",
24370
+ "min-width",
24371
+ "max-width",
24372
+ "min-height",
24373
+ "max-height",
24374
+ "top",
24375
+ "left",
24376
+ "right",
24377
+ "bottom",
24378
+ "inset",
24379
+ "inset-block",
24380
+ "inset-inline",
24381
+ "margin",
24382
+ "margin-top",
24383
+ "margin-right",
24384
+ "margin-bottom",
24385
+ "margin-left",
24386
+ "margin-block",
24387
+ "margin-inline",
24388
+ "padding",
24389
+ "padding-top",
24390
+ "padding-right",
24391
+ "padding-bottom",
24392
+ "padding-left",
24393
+ "padding-block",
24394
+ "padding-inline"
24395
+ ]);
24396
+ const noTailwindLayoutTransition = defineRule({
24397
+ id: "no-tailwind-layout-transition",
24398
+ title: "Animating a layout property",
24399
+ tags: ["design", "test-noise"],
24400
+ severity: "warn",
24401
+ category: "Performance",
24402
+ recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
24403
+ create: (context) => ({ JSXOpeningElement(node) {
24404
+ const classNameValue = getStringFromClassNameAttr(node);
24405
+ if (!classNameValue) return;
24406
+ for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
24407
+ const animatedProperties = transitionMatch[1];
24408
+ const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
24409
+ if (layoutProperty) context.report({
24410
+ node,
24411
+ message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
24412
+ });
24413
+ }
24414
+ } })
24415
+ });
24416
+ //#endregion
24417
+ //#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
24418
+ const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
24419
+ const targetIsBlank = (attribute) => {
24420
+ const stringValue = getJsxPropStringValue(attribute);
24421
+ if (stringValue !== null) return stringValue === "_blank";
24422
+ const value = attribute.value;
24423
+ if (value && isNodeOfType(value, "JSXExpressionContainer")) {
24424
+ const expression = value.expression;
24425
+ if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
24426
+ }
24427
+ return false;
24428
+ };
24429
+ const noTargetBlankWithoutRel = defineRule({
24430
+ id: "no-target-blank-without-rel",
24431
+ title: "target=_blank without rel=noopener",
24432
+ severity: "warn",
24433
+ recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
24434
+ create: (context) => ({ JSXOpeningElement(node) {
24435
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
24436
+ const tagName = node.name.name;
24437
+ if (tagName !== "a" && tagName !== "area") return;
24438
+ if (hasJsxSpreadAttribute(node.attributes)) return;
24439
+ const targetAttribute = findJsxAttribute(node.attributes, "target");
24440
+ if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
24441
+ const relAttribute = findJsxAttribute(node.attributes, "rel");
24442
+ if (relAttribute) {
24443
+ const relValue = getJsxPropStringValue(relAttribute);
24444
+ if (relValue === null) return;
24445
+ const tokens = relValue.toLowerCase().split(/\s+/);
24446
+ if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
24447
+ }
24448
+ context.report({
24449
+ node: node.name,
24450
+ message: MESSAGE$13
24451
+ });
24452
+ } })
24453
+ });
24454
+ //#endregion
23575
24455
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
23576
- const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
24456
+ const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
23577
24457
  const isInsideClassMethod = (node, customClassFactoryNames) => {
23578
24458
  let ancestor = node.parent;
23579
24459
  while (ancestor) {
@@ -23642,7 +24522,7 @@ const noThisInSfc = defineRule({
23642
24522
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
23643
24523
  context.report({
23644
24524
  node,
23645
- message: MESSAGE$10
24525
+ message: MESSAGE$12
23646
24526
  });
23647
24527
  } };
23648
24528
  }
@@ -23680,26 +24560,39 @@ const noTinyText = defineRule({
23680
24560
  });
23681
24561
  //#endregion
23682
24562
  //#region src/plugin/rules/performance/no-transition-all.ts
24563
+ const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
24564
+ const TAILWIND_MESSAGE = "Your users see janky animation because `transition-all` animates every property that changes, including expensive layout ones and instant ones like focus rings. Name the properties: `transition-colors`, `transition-opacity`, or `transition-transform`.";
23683
24565
  const noTransitionAll = defineRule({
23684
24566
  id: "no-transition-all",
23685
24567
  title: "transition: all animates everything",
23686
24568
  tags: ["test-noise"],
23687
24569
  severity: "warn",
23688
24570
  recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
23689
- create: (context) => ({ JSXAttribute(node) {
23690
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
23691
- if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
23692
- const expression = node.value.expression;
23693
- if (!isNodeOfType(expression, "ObjectExpression")) return;
23694
- for (const property of expression.properties ?? []) {
23695
- if (!isNodeOfType(property, "Property")) continue;
23696
- if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
23697
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
23698
- node: property,
23699
- message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24571
+ create: (context) => ({
24572
+ JSXAttribute(node) {
24573
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
24574
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24575
+ const expression = node.value.expression;
24576
+ if (!isNodeOfType(expression, "ObjectExpression")) return;
24577
+ for (const property of expression.properties ?? []) {
24578
+ if (!isNodeOfType(property, "Property")) continue;
24579
+ const key = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
24580
+ if (key !== "transition" && key !== "transitionProperty") continue;
24581
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.trim().startsWith("all")) context.report({
24582
+ node: property,
24583
+ message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24584
+ });
24585
+ }
24586
+ },
24587
+ JSXOpeningElement(node) {
24588
+ const classNameValue = getStringFromClassNameAttr(node);
24589
+ if (!classNameValue) return;
24590
+ if (hasTransitionAllClass(classNameValue)) context.report({
24591
+ node,
24592
+ message: TAILWIND_MESSAGE
23700
24593
  });
23701
24594
  }
23702
- } })
24595
+ })
23703
24596
  });
23704
24597
  //#endregion
23705
24598
  //#region src/plugin/rules/correctness/no-uncontrolled-input.ts
@@ -23743,7 +24636,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
23743
24636
  }
23744
24637
  return stateNames;
23745
24638
  };
23746
- const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
23747
24639
  const noUncontrolledInput = defineRule({
23748
24640
  id: "no-uncontrolled-input",
23749
24641
  title: "Uncontrolled input value",
@@ -23847,6 +24739,38 @@ const noUnescapedEntities = defineRule({
23847
24739
  } })
23848
24740
  });
23849
24741
  //#endregion
24742
+ //#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
24743
+ const UNINFORMATIVE_LABELS = new Set([
24744
+ "icon",
24745
+ "button",
24746
+ "image",
24747
+ "img",
24748
+ "link",
24749
+ "graphic",
24750
+ "svg",
24751
+ "picture",
24752
+ "element",
24753
+ "field",
24754
+ "input"
24755
+ ]);
24756
+ const MESSAGE$11 = "An `aria-label` should name the action or destination, not the element type — this value tells screen-reader users nothing. Use something like `aria-label=\"Search\"` or `aria-label=\"Close dialog\"`.";
24757
+ const noUninformativeAriaLabel = defineRule({
24758
+ id: "no-uninformative-aria-label",
24759
+ title: "Uninformative aria-label",
24760
+ severity: "warn",
24761
+ recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
24762
+ create: (context) => ({ JSXOpeningElement(node) {
24763
+ const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
24764
+ if (!ariaLabel) return;
24765
+ const labelValue = getJsxPropStringValue(ariaLabel);
24766
+ if (labelValue === null) return;
24767
+ if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
24768
+ node: ariaLabel,
24769
+ message: MESSAGE$11
24770
+ });
24771
+ } })
24772
+ });
24773
+ //#endregion
23850
24774
  //#region src/plugin/constants/dom-aria-properties.ts
23851
24775
  const ARIA_PROPERTY_NAMES = new Set([
23852
24776
  "activedescendant",
@@ -25318,7 +26242,7 @@ const noWideLetterSpacing = defineRule({
25318
26242
  //#endregion
25319
26243
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
25320
26244
  const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
25321
- const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
26245
+ const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
25322
26246
  const resolveSettings$7 = (settings) => {
25323
26247
  const reactDoctor = settings?.["react-doctor"];
25324
26248
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -25352,7 +26276,7 @@ const noWillUpdateSetState = defineRule({
25352
26276
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25353
26277
  context.report({
25354
26278
  node: node.callee,
25355
- message: MESSAGE$9
26279
+ message: MESSAGE$10
25356
26280
  });
25357
26281
  } };
25358
26282
  }
@@ -26230,7 +27154,7 @@ const preactNoRenderArguments = defineRule({
26230
27154
  });
26231
27155
  //#endregion
26232
27156
  //#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
26233
- const MESSAGE$8 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
27157
+ const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
26234
27158
  const preactPreferOndblclick = defineRule({
26235
27159
  id: "preact-prefer-ondblclick",
26236
27160
  title: "onDoubleClick instead of onDblClick",
@@ -26245,7 +27169,7 @@ const preactPreferOndblclick = defineRule({
26245
27169
  if (!onDoubleClickAttribute) return;
26246
27170
  context.report({
26247
27171
  node: onDoubleClickAttribute,
26248
- message: MESSAGE$8
27172
+ message: MESSAGE$9
26249
27173
  });
26250
27174
  } })
26251
27175
  });
@@ -26285,6 +27209,42 @@ const preactPreferOninput = defineRule({
26285
27209
  } })
26286
27210
  });
26287
27211
  //#endregion
27212
+ //#region src/plugin/rules/design/prefer-dvh-over-vh.ts
27213
+ const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
27214
+ const HEIGHT_KEYS = new Set(["height", "minHeight"]);
27215
+ const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
27216
+ const preferDvhOverVh = defineRule({
27217
+ id: "prefer-dvh-over-vh",
27218
+ title: "Use dvh instead of vh for full height",
27219
+ tags: ["design", "test-noise"],
27220
+ severity: "warn",
27221
+ requires: ["tailwind:3.4"],
27222
+ recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
27223
+ create: (context) => ({
27224
+ JSXAttribute(node) {
27225
+ const expression = getInlineStyleExpression(node);
27226
+ if (!expression) return;
27227
+ for (const property of expression.properties ?? []) {
27228
+ const key = getStylePropertyKey(property);
27229
+ if (!key || !HEIGHT_KEYS.has(key)) continue;
27230
+ const value = getStylePropertyStringValue(property);
27231
+ if (value && value.trim().toLowerCase() === "100vh") context.report({
27232
+ node: property,
27233
+ message: MESSAGE$8
27234
+ });
27235
+ }
27236
+ },
27237
+ JSXOpeningElement(node) {
27238
+ const classNameValue = getStringFromClassNameAttr(node);
27239
+ if (!classNameValue) return;
27240
+ if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
27241
+ node,
27242
+ message: MESSAGE$8
27243
+ });
27244
+ }
27245
+ })
27246
+ });
27247
+ //#endregion
26288
27248
  //#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
26289
27249
  const preferDynamicImport = defineRule({
26290
27250
  id: "prefer-dynamic-import",
@@ -26876,6 +27836,26 @@ const preferTagOverRole = defineRule({
26876
27836
  } })
26877
27837
  });
26878
27838
  //#endregion
27839
+ //#region src/plugin/rules/design/prefer-truncate-shorthand.ts
27840
+ const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
27841
+ const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
27842
+ const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
27843
+ const preferTruncateShorthand = defineRule({
27844
+ id: "prefer-truncate-shorthand",
27845
+ title: "Use truncate shorthand",
27846
+ tags: ["design", "test-noise"],
27847
+ severity: "warn",
27848
+ recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
27849
+ create: (context) => ({ JSXOpeningElement(node) {
27850
+ const classNameValue = getStringFromClassNameAttr(node);
27851
+ if (!classNameValue) return;
27852
+ if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
27853
+ node,
27854
+ message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
27855
+ });
27856
+ } })
27857
+ });
27858
+ //#endregion
26879
27859
  //#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
26880
27860
  const collectFunctionTypedLocalBindings = (componentBody) => {
26881
27861
  const functionTypedLocals = /* @__PURE__ */ new Set();
@@ -34681,6 +35661,47 @@ const serverAfterNonblocking = defineRule({
34681
35661
  }
34682
35662
  });
34683
35663
  //#endregion
35664
+ //#region src/plugin/utils/is-auth-guard-name.ts
35665
+ const SIGNED_IN_HEAD_TOKENS = new Set([
35666
+ "signed",
35667
+ "logged",
35668
+ "sign"
35669
+ ]);
35670
+ const mergeSignedInTokens = (tokens) => {
35671
+ const mergedTokens = [];
35672
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
35673
+ const currentToken = tokens[tokenIndex];
35674
+ if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
35675
+ mergedTokens.push(`${currentToken}in`);
35676
+ tokenIndex += 1;
35677
+ continue;
35678
+ }
35679
+ mergedTokens.push(currentToken);
35680
+ }
35681
+ return mergedTokens;
35682
+ };
35683
+ const isAuthGuardName = (calleeName) => {
35684
+ const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
35685
+ if (tokens.length === 0) return false;
35686
+ let hasAssertiveVerb = false;
35687
+ let hasGetterVerb = false;
35688
+ let hasQualifier = false;
35689
+ let hasStrongNoun = false;
35690
+ let hasWeakNoun = false;
35691
+ for (const token of tokens) {
35692
+ if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
35693
+ if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
35694
+ if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
35695
+ if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
35696
+ if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
35697
+ if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
35698
+ }
35699
+ if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
35700
+ if (hasGetterVerb && hasStrongNoun) return true;
35701
+ if (hasQualifier && hasWeakNoun) return true;
35702
+ return false;
35703
+ };
35704
+ //#endregion
34684
35705
  //#region src/plugin/rules/server/server-auth-actions.ts
34685
35706
  const isAsyncFunctionLikeNode = (node) => {
34686
35707
  if (!node) return false;
@@ -34723,9 +35744,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
34723
35744
  const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
34724
35745
  const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
34725
35746
  if (!calleeNode) return null;
34726
- if (isNodeOfType(calleeNode, "Identifier")) return allowedFunctionNames.has(calleeNode.name) ? calleeNode.name : null;
35747
+ if (isNodeOfType(calleeNode, "Identifier")) {
35748
+ const calleeName = calleeNode.name;
35749
+ return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
35750
+ }
34727
35751
  if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
34728
35752
  const methodName = calleeNode.property.name;
35753
+ if (isAuthGuardName(methodName)) return methodName;
34729
35754
  if (!allowedFunctionNames.has(methodName)) return null;
34730
35755
  if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
34731
35756
  return methodName;
@@ -35102,13 +36127,7 @@ const serverNoMutableModuleState = defineRule({
35102
36127
  const collectDeclaredNames = (declaration) => {
35103
36128
  const names = /* @__PURE__ */ new Set();
35104
36129
  if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
35105
- for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier")) names.add(declarator.id.name);
35106
- else if (isNodeOfType(declarator.id, "ObjectPattern")) {
35107
- for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
35108
- else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
35109
- } else if (isNodeOfType(declarator.id, "ArrayPattern")) {
35110
- for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
35111
- }
36130
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
35112
36131
  return names;
35113
36132
  };
35114
36133
  const declarationStartsWithAwait = (declaration) => {
@@ -35118,11 +36137,15 @@ const declarationStartsWithAwait = (declaration) => {
35118
36137
  };
35119
36138
  const declarationReadsAnyName = (declaration, names) => {
35120
36139
  if (names.size === 0) return false;
36140
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
35121
36141
  let didRead = false;
35122
- walkAst(declaration, (child) => {
35123
- if (didRead) return;
35124
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35125
- });
36142
+ for (const declarator of declaration.declarations ?? []) {
36143
+ if (!declarator.init) continue;
36144
+ walkAst(declarator.init, (child) => {
36145
+ if (didRead) return;
36146
+ if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
36147
+ });
36148
+ }
35126
36149
  return didRead;
35127
36150
  };
35128
36151
  const serverSequentialIndependentAwait = defineRule({
@@ -36382,7 +37405,7 @@ const urlPrefilledPrivilegedAction = defineRule({
36382
37405
  recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
36383
37406
  scan: scanByPattern({
36384
37407
  shouldScan: (file) => isClientSourcePath(file.relativePath),
36385
- pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
37408
+ pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
36386
37409
  message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
36387
37410
  })
36388
37411
  });
@@ -37144,6 +38167,17 @@ const reactDoctorRules = [
37144
38167
  category: "Performance"
37145
38168
  }
37146
38169
  },
38170
+ {
38171
+ key: "react-doctor/auth-token-in-web-storage",
38172
+ id: "auth-token-in-web-storage",
38173
+ source: "react-doctor",
38174
+ originallyExternal: false,
38175
+ rule: {
38176
+ ...authTokenInWebStorage,
38177
+ framework: "global",
38178
+ category: "Security"
38179
+ }
38180
+ },
37147
38181
  {
37148
38182
  key: "react-doctor/autocomplete-valid",
37149
38183
  id: "autocomplete-valid",
@@ -37360,6 +38394,18 @@ const reactDoctorRules = [
37360
38394
  requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
37361
38395
  }
37362
38396
  },
38397
+ {
38398
+ key: "react-doctor/dialog-has-accessible-name",
38399
+ id: "dialog-has-accessible-name",
38400
+ source: "react-doctor",
38401
+ originallyExternal: false,
38402
+ rule: {
38403
+ ...dialogHasAccessibleName,
38404
+ framework: "global",
38405
+ category: "Accessibility",
38406
+ requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
38407
+ }
38408
+ },
37363
38409
  {
37364
38410
  key: "react-doctor/display-name",
37365
38411
  id: "display-name",
@@ -38484,6 +39530,17 @@ const reactDoctorRules = [
38484
39530
  requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
38485
39531
  }
38486
39532
  },
39533
+ {
39534
+ key: "react-doctor/no-arbitrary-px-font-size",
39535
+ id: "no-arbitrary-px-font-size",
39536
+ source: "react-doctor",
39537
+ originallyExternal: false,
39538
+ rule: {
39539
+ ...noArbitraryPxFontSize,
39540
+ framework: "global",
39541
+ category: "Accessibility"
39542
+ }
39543
+ },
38487
39544
  {
38488
39545
  key: "react-doctor/no-aria-hidden-on-focusable",
38489
39546
  id: "no-aria-hidden-on-focusable",
@@ -38519,6 +39576,18 @@ const reactDoctorRules = [
38519
39576
  requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
38520
39577
  }
38521
39578
  },
39579
+ {
39580
+ key: "react-doctor/no-async-effect-callback",
39581
+ id: "no-async-effect-callback",
39582
+ source: "react-doctor",
39583
+ originallyExternal: false,
39584
+ rule: {
39585
+ ...noAsyncEffectCallback,
39586
+ framework: "global",
39587
+ category: "Bugs",
39588
+ requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
39589
+ }
39590
+ },
38522
39591
  {
38523
39592
  key: "react-doctor/no-autofocus",
38524
39593
  id: "no-autofocus",
@@ -38531,6 +39600,18 @@ const reactDoctorRules = [
38531
39600
  requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
38532
39601
  }
38533
39602
  },
39603
+ {
39604
+ key: "react-doctor/no-autoplay-without-muted",
39605
+ id: "no-autoplay-without-muted",
39606
+ source: "react-doctor",
39607
+ originallyExternal: false,
39608
+ rule: {
39609
+ ...noAutoplayWithoutMuted,
39610
+ framework: "global",
39611
+ category: "Accessibility",
39612
+ requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
39613
+ }
39614
+ },
38534
39615
  {
38535
39616
  key: "react-doctor/no-barrel-import",
38536
39617
  id: "no-barrel-import",
@@ -38542,6 +39623,18 @@ const reactDoctorRules = [
38542
39623
  category: "Performance"
38543
39624
  }
38544
39625
  },
39626
+ {
39627
+ key: "react-doctor/no-call-component-as-function",
39628
+ id: "no-call-component-as-function",
39629
+ source: "react-doctor",
39630
+ originallyExternal: false,
39631
+ rule: {
39632
+ ...noCallComponentAsFunction,
39633
+ framework: "global",
39634
+ category: "Bugs",
39635
+ requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
39636
+ }
39637
+ },
38545
39638
  {
38546
39639
  key: "react-doctor/no-cascading-set-state",
38547
39640
  id: "no-cascading-set-state",
@@ -38602,6 +39695,18 @@ const reactDoctorRules = [
38602
39695
  requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
38603
39696
  }
38604
39697
  },
39698
+ {
39699
+ key: "react-doctor/no-create-ref-in-function-component",
39700
+ id: "no-create-ref-in-function-component",
39701
+ source: "react-doctor",
39702
+ originallyExternal: false,
39703
+ rule: {
39704
+ ...noCreateRefInFunctionComponent,
39705
+ framework: "global",
39706
+ category: "Bugs",
39707
+ requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
39708
+ }
39709
+ },
38605
39710
  {
38606
39711
  key: "react-doctor/no-create-store-in-render",
38607
39712
  id: "no-create-store-in-render",
@@ -38660,6 +39765,17 @@ const reactDoctorRules = [
38660
39765
  category: "Maintainability"
38661
39766
  }
38662
39767
  },
39768
+ {
39769
+ key: "react-doctor/no-deprecated-tailwind-class",
39770
+ id: "no-deprecated-tailwind-class",
39771
+ source: "react-doctor",
39772
+ originallyExternal: false,
39773
+ rule: {
39774
+ ...noDeprecatedTailwindClass,
39775
+ framework: "global",
39776
+ category: "Maintainability"
39777
+ }
39778
+ },
38663
39779
  {
38664
39780
  key: "react-doctor/no-derived-state",
38665
39781
  id: "no-derived-state",
@@ -38779,6 +39895,17 @@ const reactDoctorRules = [
38779
39895
  requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
38780
39896
  }
38781
39897
  },
39898
+ {
39899
+ key: "react-doctor/no-document-write",
39900
+ id: "no-document-write",
39901
+ source: "react-doctor",
39902
+ originallyExternal: false,
39903
+ rule: {
39904
+ ...noDocumentWrite,
39905
+ framework: "global",
39906
+ category: "Performance"
39907
+ }
39908
+ },
38782
39909
  {
38783
39910
  key: "react-doctor/no-dynamic-import-path",
38784
39911
  id: "no-dynamic-import-path",
@@ -38920,6 +40047,17 @@ const reactDoctorRules = [
38920
40047
  category: "Performance"
38921
40048
  }
38922
40049
  },
40050
+ {
40051
+ key: "react-doctor/no-full-viewport-width",
40052
+ id: "no-full-viewport-width",
40053
+ source: "react-doctor",
40054
+ originallyExternal: false,
40055
+ rule: {
40056
+ ...noFullViewportWidth,
40057
+ framework: "global",
40058
+ category: "Maintainability"
40059
+ }
40060
+ },
38923
40061
  {
38924
40062
  key: "react-doctor/no-generic-handler-names",
38925
40063
  id: "no-generic-handler-names",
@@ -38976,6 +40114,18 @@ const reactDoctorRules = [
38976
40114
  category: "Accessibility"
38977
40115
  }
38978
40116
  },
40117
+ {
40118
+ key: "react-doctor/no-img-lazy-with-high-fetchpriority",
40119
+ id: "no-img-lazy-with-high-fetchpriority",
40120
+ source: "react-doctor",
40121
+ originallyExternal: false,
40122
+ rule: {
40123
+ ...noImgLazyWithHighFetchpriority,
40124
+ framework: "global",
40125
+ category: "Performance",
40126
+ requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
40127
+ }
40128
+ },
38979
40129
  {
38980
40130
  key: "react-doctor/no-initialize-state",
38981
40131
  id: "no-initialize-state",
@@ -39046,6 +40196,17 @@ const reactDoctorRules = [
39046
40196
  requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
39047
40197
  }
39048
40198
  },
40199
+ {
40200
+ key: "react-doctor/no-json-parse-stringify-clone",
40201
+ id: "no-json-parse-stringify-clone",
40202
+ source: "react-doctor",
40203
+ originallyExternal: false,
40204
+ rule: {
40205
+ ...noJsonParseStringifyClone,
40206
+ framework: "global",
40207
+ category: "Performance"
40208
+ }
40209
+ },
39049
40210
  {
39050
40211
  key: "react-doctor/no-jsx-element-type",
39051
40212
  id: "no-jsx-element-type",
@@ -39136,6 +40297,17 @@ const reactDoctorRules = [
39136
40297
  category: "Performance"
39137
40298
  }
39138
40299
  },
40300
+ {
40301
+ key: "react-doctor/no-low-contrast-inline-style",
40302
+ id: "no-low-contrast-inline-style",
40303
+ source: "react-doctor",
40304
+ originallyExternal: false,
40305
+ rule: {
40306
+ ...noLowContrastInlineStyle,
40307
+ framework: "global",
40308
+ category: "Accessibility"
40309
+ }
40310
+ },
39139
40311
  {
39140
40312
  key: "react-doctor/no-many-boolean-props",
39141
40313
  id: "no-many-boolean-props",
@@ -39413,6 +40585,17 @@ const reactDoctorRules = [
39413
40585
  category: "Maintainability"
39414
40586
  }
39415
40587
  },
40588
+ {
40589
+ key: "react-doctor/no-redundant-display-class",
40590
+ id: "no-redundant-display-class",
40591
+ source: "react-doctor",
40592
+ originallyExternal: false,
40593
+ rule: {
40594
+ ...noRedundantDisplayClass,
40595
+ framework: "global",
40596
+ category: "Maintainability"
40597
+ }
40598
+ },
39416
40599
  {
39417
40600
  key: "react-doctor/no-redundant-roles",
39418
40601
  id: "no-redundant-roles",
@@ -39565,6 +40748,18 @@ const reactDoctorRules = [
39565
40748
  requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
39566
40749
  }
39567
40750
  },
40751
+ {
40752
+ key: "react-doctor/no-string-false-on-boolean-attribute",
40753
+ id: "no-string-false-on-boolean-attribute",
40754
+ source: "react-doctor",
40755
+ originallyExternal: false,
40756
+ rule: {
40757
+ ...noStringFalseOnBooleanAttribute,
40758
+ framework: "global",
40759
+ category: "Bugs",
40760
+ requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
40761
+ }
40762
+ },
39568
40763
  {
39569
40764
  key: "react-doctor/no-string-refs",
39570
40765
  id: "no-string-refs",
@@ -39577,6 +40772,51 @@ const reactDoctorRules = [
39577
40772
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
39578
40773
  }
39579
40774
  },
40775
+ {
40776
+ key: "react-doctor/no-svg-currentcolor-with-fill-class",
40777
+ id: "no-svg-currentcolor-with-fill-class",
40778
+ source: "react-doctor",
40779
+ originallyExternal: false,
40780
+ rule: {
40781
+ ...noSvgCurrentcolorWithFillClass,
40782
+ framework: "global",
40783
+ category: "Maintainability"
40784
+ }
40785
+ },
40786
+ {
40787
+ key: "react-doctor/no-sync-xhr",
40788
+ id: "no-sync-xhr",
40789
+ source: "react-doctor",
40790
+ originallyExternal: false,
40791
+ rule: {
40792
+ ...noSyncXhr,
40793
+ framework: "global",
40794
+ category: "Performance"
40795
+ }
40796
+ },
40797
+ {
40798
+ key: "react-doctor/no-tailwind-layout-transition",
40799
+ id: "no-tailwind-layout-transition",
40800
+ source: "react-doctor",
40801
+ originallyExternal: false,
40802
+ rule: {
40803
+ ...noTailwindLayoutTransition,
40804
+ framework: "global",
40805
+ category: "Performance"
40806
+ }
40807
+ },
40808
+ {
40809
+ key: "react-doctor/no-target-blank-without-rel",
40810
+ id: "no-target-blank-without-rel",
40811
+ source: "react-doctor",
40812
+ originallyExternal: false,
40813
+ rule: {
40814
+ ...noTargetBlankWithoutRel,
40815
+ framework: "global",
40816
+ category: "Accessibility",
40817
+ requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
40818
+ }
40819
+ },
39580
40820
  {
39581
40821
  key: "react-doctor/no-this-in-sfc",
39582
40822
  id: "no-this-in-sfc",
@@ -39646,6 +40886,18 @@ const reactDoctorRules = [
39646
40886
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
39647
40887
  }
39648
40888
  },
40889
+ {
40890
+ key: "react-doctor/no-uninformative-aria-label",
40891
+ id: "no-uninformative-aria-label",
40892
+ source: "react-doctor",
40893
+ originallyExternal: false,
40894
+ rule: {
40895
+ ...noUninformativeAriaLabel,
40896
+ framework: "global",
40897
+ category: "Accessibility",
40898
+ requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
40899
+ }
40900
+ },
39649
40901
  {
39650
40902
  key: "react-doctor/no-unknown-property",
39651
40903
  id: "no-unknown-property",
@@ -39855,6 +41107,17 @@ const reactDoctorRules = [
39855
41107
  category: "Bugs"
39856
41108
  }
39857
41109
  },
41110
+ {
41111
+ key: "react-doctor/prefer-dvh-over-vh",
41112
+ id: "prefer-dvh-over-vh",
41113
+ source: "react-doctor",
41114
+ originallyExternal: false,
41115
+ rule: {
41116
+ ...preferDvhOverVh,
41117
+ framework: "global",
41118
+ category: "Maintainability"
41119
+ }
41120
+ },
39858
41121
  {
39859
41122
  key: "react-doctor/prefer-dynamic-import",
39860
41123
  id: "prefer-dynamic-import",
@@ -39959,6 +41222,17 @@ const reactDoctorRules = [
39959
41222
  requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
39960
41223
  }
39961
41224
  },
41225
+ {
41226
+ key: "react-doctor/prefer-truncate-shorthand",
41227
+ id: "prefer-truncate-shorthand",
41228
+ source: "react-doctor",
41229
+ originallyExternal: false,
41230
+ rule: {
41231
+ ...preferTruncateShorthand,
41232
+ framework: "global",
41233
+ category: "Maintainability"
41234
+ }
41235
+ },
39962
41236
  {
39963
41237
  key: "react-doctor/prefer-use-effect-event",
39964
41238
  id: "prefer-use-effect-event",