oxlint-plugin-react-doctor 0.7.2-dev.0eb5293 → 0.7.2-dev.2953b25

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 +92 -0
  2. package/dist/index.js +1764 -549
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -532,6 +532,12 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
532
532
  ]);
533
533
  const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
534
534
  const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
535
+ const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
536
+ "WebSocket",
537
+ "EventSource",
538
+ "BroadcastChannel",
539
+ "RTCPeerConnection"
540
+ ]);
535
541
  const MUTABLE_GLOBAL_ROOTS = new Set([
536
542
  "location",
537
543
  "window",
@@ -595,6 +601,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
595
601
  "parseInt",
596
602
  "parseFloat"
597
603
  ]);
604
+ const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
605
+ "Date",
606
+ "Map",
607
+ "Set",
608
+ "WeakMap",
609
+ "WeakSet",
610
+ "WeakRef",
611
+ "RegExp",
612
+ "Error",
613
+ "URL",
614
+ "URLSearchParams",
615
+ "AbortController"
616
+ ]);
598
617
  const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
599
618
  "Boolean",
600
619
  "String",
@@ -674,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
674
693
  "unwatch",
675
694
  "unlisten",
676
695
  "unsub",
677
- "abort"
696
+ "abort",
697
+ "disconnect",
698
+ "unobserve",
699
+ "close"
678
700
  ]);
679
701
  const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
680
702
  "remove",
@@ -2235,6 +2257,39 @@ const anchorHasContent = defineRule({
2235
2257
  }
2236
2258
  });
2237
2259
  //#endregion
2260
+ //#region src/plugin/utils/get-jsx-prop-static-string-values.ts
2261
+ const MAX_CONST_RESOLUTION_HOPS = 4;
2262
+ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2263
+ const expression = stripParenExpression(rawExpression);
2264
+ if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
2265
+ if (isNodeOfType(expression, "TemplateLiteral")) {
2266
+ const staticValue = getStaticTemplateLiteralValue(expression);
2267
+ return staticValue === null ? null : [staticValue];
2268
+ }
2269
+ if (isNodeOfType(expression, "ConditionalExpression")) {
2270
+ const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
2271
+ if (consequentValues === null) return null;
2272
+ const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
2273
+ if (alternateValues === null) return null;
2274
+ return [...consequentValues, ...alternateValues];
2275
+ }
2276
+ if (isNodeOfType(expression, "Identifier")) {
2277
+ if (remainingHops === 0) return null;
2278
+ const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2279
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2280
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
2281
+ return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
2282
+ }
2283
+ return null;
2284
+ };
2285
+ const getJsxPropStaticStringValues = (attribute, scopes) => {
2286
+ const value = attribute.value;
2287
+ if (!value) return null;
2288
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
2289
+ if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
2290
+ return null;
2291
+ };
2292
+ //#endregion
2238
2293
  //#region src/plugin/rules/a11y/anchor-is-valid.ts
2239
2294
  const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
2240
2295
  const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
@@ -2262,30 +2317,14 @@ const isDirectChildOfLinkComponent = (openingElement) => {
2262
2317
  const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
2263
2318
  const isInvalidHref = (value, validHrefs) => {
2264
2319
  if (validHrefs.has(value)) return false;
2265
- return value === "" || value === "#" || value === "javascript:void(0)";
2266
- };
2267
- const getStaticHrefValue = (value) => {
2268
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
2269
- if (isNodeOfType(value, "JSXExpressionContainer")) {
2270
- const expression = value.expression;
2271
- if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
2272
- if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
2273
- }
2274
- return null;
2320
+ const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
2321
+ return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
2275
2322
  };
2276
- const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
2277
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
2323
+ const isNullishOrFragmentHref = (value) => {
2278
2324
  if (isNodeOfType(value, "JSXExpressionContainer")) {
2279
2325
  const expression = value.expression;
2280
2326
  if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
2281
- if (isNodeOfType(expression, "Literal")) {
2282
- if (expression.value === null) return true;
2283
- if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
2284
- }
2285
- if (isNodeOfType(expression, "TemplateLiteral")) {
2286
- const staticValue = getStaticTemplateLiteralValue(expression);
2287
- return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
2288
- }
2327
+ if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
2289
2328
  }
2290
2329
  if (isNodeOfType(value, "JSXFragment")) return true;
2291
2330
  return false;
@@ -2318,9 +2357,10 @@ const anchorIsValid = defineRule({
2318
2357
  });
2319
2358
  return;
2320
2359
  }
2321
- if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
2360
+ const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
2361
+ if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
2322
2362
  const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
2323
- if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
2363
+ if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
2324
2364
  context.report({
2325
2365
  node: node.name,
2326
2366
  message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
@@ -3376,6 +3416,25 @@ const ariaRole = defineRule({
3376
3416
  if (!roleAttribute) return;
3377
3417
  const elementType = getElementType(node, context.settings);
3378
3418
  if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
3419
+ const reportFirstInvalidCandidate = (candidates) => {
3420
+ for (const candidate of candidates) {
3421
+ if (candidate.trim().length === 0) {
3422
+ context.report({
3423
+ node: roleAttribute,
3424
+ message: buildBaseMessage("")
3425
+ });
3426
+ return;
3427
+ }
3428
+ const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
3429
+ for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3430
+ context.report({
3431
+ node: roleAttribute,
3432
+ message: buildBaseMessage(` \`${token}\` is not one.`)
3433
+ });
3434
+ return;
3435
+ }
3436
+ }
3437
+ };
3379
3438
  const value = roleAttribute.value;
3380
3439
  if (!value) {
3381
3440
  context.report({
@@ -3392,22 +3451,7 @@ const ariaRole = defineRule({
3392
3451
  });
3393
3452
  return;
3394
3453
  }
3395
- const stringValue = value.value;
3396
- if (stringValue.trim().length === 0) {
3397
- context.report({
3398
- node: roleAttribute,
3399
- message: buildBaseMessage("")
3400
- });
3401
- return;
3402
- }
3403
- const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
3404
- for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3405
- context.report({
3406
- node: roleAttribute,
3407
- message: buildBaseMessage(` \`${token}\` is not one.`)
3408
- });
3409
- return;
3410
- }
3454
+ reportFirstInvalidCandidate([value.value]);
3411
3455
  return;
3412
3456
  }
3413
3457
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -3428,6 +3472,11 @@ const ariaRole = defineRule({
3428
3472
  });
3429
3473
  return;
3430
3474
  }
3475
+ const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
3476
+ if (resolvedCandidates !== null) {
3477
+ reportFirstInvalidCandidate(resolvedCandidates);
3478
+ return;
3479
+ }
3431
3480
  return;
3432
3481
  }
3433
3482
  context.report({
@@ -5144,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
5144
5193
  const callee = node.callee;
5145
5194
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
5146
5195
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
5147
- if (!isWebStorageObject(callee.object)) return;
5196
+ if (!isWebStorageObject(stripParenExpression(callee.object))) return;
5148
5197
  const keyArgument = node.arguments?.[0];
5149
5198
  if (!keyArgument) return;
5150
5199
  const keyString = resolveStaticKeyString(keyArgument);
@@ -6084,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
6084
6133
  })
6085
6134
  });
6086
6135
  //#endregion
6136
+ //#region src/plugin/utils/is-global-method-call.ts
6137
+ const isGlobalMethodCall = (node, objectName, methodName) => {
6138
+ if (!isNodeOfType(node, "CallExpression")) return false;
6139
+ const callee = stripParenExpression(node.callee);
6140
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
6141
+ const receiver = stripParenExpression(callee.object);
6142
+ return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
6143
+ };
6144
+ //#endregion
6087
6145
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6088
6146
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6089
6147
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6090
6148
  const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6091
6149
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6092
- const isJsonStringifyCall = (node) => {
6093
- if (!isNodeOfType(node, "CallExpression")) return false;
6094
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
6095
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
6096
- if (node.callee.object.name !== "JSON") return false;
6097
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
6098
- return node.callee.property.name === "stringify";
6099
- };
6150
+ const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6100
6151
  const resolveStringKey = (keyArg, context) => {
6101
6152
  if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
6102
6153
  if (!isNodeOfType(keyArg, "Identifier")) return null;
@@ -6115,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
6115
6166
  recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
6116
6167
  create: (context) => ({ CallExpression(node) {
6117
6168
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6118
- if (!isNodeOfType(node.callee.object, "Identifier")) return;
6119
- if (!STORAGE_OBJECTS.has(node.callee.object.name)) return;
6169
+ const receiver = stripParenExpression(node.callee.object);
6170
+ if (!isNodeOfType(receiver, "Identifier")) return;
6171
+ if (!STORAGE_OBJECTS.has(receiver.name)) return;
6120
6172
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6121
6173
  if (node.callee.property.name !== "setItem") return;
6122
6174
  const keyArg = node.arguments?.[0];
@@ -6129,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
6129
6181
  if (!isJsonStringifyCall(valueArg)) return;
6130
6182
  context.report({
6131
6183
  node: keyArg,
6132
- message: `${node.callee.object.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6184
+ message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6133
6185
  });
6134
6186
  } })
6135
6187
  });
@@ -7610,6 +7662,102 @@ const displayName = defineRule({
7610
7662
  }
7611
7663
  });
7612
7664
  //#endregion
7665
+ //#region src/plugin/utils/is-react-hook-name.ts
7666
+ const isReactHookName = (name) => {
7667
+ if (!name.startsWith("use")) return false;
7668
+ if (name.length === 3) return true;
7669
+ const fourthCharacter = name.charCodeAt(3);
7670
+ return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
7671
+ };
7672
+ //#endregion
7673
+ //#region src/plugin/utils/is-react-component-or-hook-name.ts
7674
+ const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
7675
+ //#endregion
7676
+ //#region src/plugin/utils/component-or-hook-display-name.ts
7677
+ const hocWrapperCalleeName = (callee) => {
7678
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
7679
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
7680
+ return null;
7681
+ };
7682
+ const displayNameFromFunctionBinding = (functionNode) => {
7683
+ let current = functionNode;
7684
+ for (;;) {
7685
+ const parent = current.parent;
7686
+ if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
7687
+ const calleeName = hocWrapperCalleeName(parent.callee);
7688
+ if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
7689
+ current = parent;
7690
+ continue;
7691
+ }
7692
+ }
7693
+ break;
7694
+ }
7695
+ const binding = current.parent;
7696
+ if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
7697
+ return null;
7698
+ };
7699
+ const componentOrHookDisplayNameForFunction = (functionNode) => {
7700
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
7701
+ return displayNameFromFunctionBinding(functionNode);
7702
+ };
7703
+ //#endregion
7704
+ //#region src/plugin/utils/find-enclosing-function.ts
7705
+ const findEnclosingFunction = (node) => {
7706
+ let cursor = node.parent;
7707
+ while (cursor) {
7708
+ if (isFunctionLike$1(cursor)) return cursor;
7709
+ cursor = cursor.parent ?? null;
7710
+ }
7711
+ return null;
7712
+ };
7713
+ //#endregion
7714
+ //#region src/plugin/utils/enclosing-component-or-hook-name.ts
7715
+ const enclosingComponentOrHookName = (node) => {
7716
+ const functionNode = findEnclosingFunction(node);
7717
+ return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
7718
+ };
7719
+ //#endregion
7720
+ //#region src/plugin/utils/get-range-start.ts
7721
+ const getRangeStart = (node) => {
7722
+ const rangeStart = node.range?.[0];
7723
+ return typeof rangeStart === "number" ? rangeStart : null;
7724
+ };
7725
+ //#endregion
7726
+ //#region src/plugin/utils/is-result-discarded-call.ts
7727
+ const isResultDiscardedCall = (callExpression) => {
7728
+ let node = callExpression;
7729
+ let parent = node.parent;
7730
+ while (parent) {
7731
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
7732
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
7733
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
7734
+ if (isNodeOfType(parent, "ChainExpression")) {
7735
+ node = parent;
7736
+ parent = node.parent;
7737
+ continue;
7738
+ }
7739
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
7740
+ node = parent;
7741
+ parent = node.parent;
7742
+ continue;
7743
+ }
7744
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
7745
+ node = parent;
7746
+ parent = node.parent;
7747
+ continue;
7748
+ }
7749
+ if (isNodeOfType(parent, "SequenceExpression")) {
7750
+ const expressions = parent.expressions ?? [];
7751
+ if (expressions[expressions.length - 1] !== node) return true;
7752
+ node = parent;
7753
+ parent = node.parent;
7754
+ continue;
7755
+ }
7756
+ return false;
7757
+ }
7758
+ return false;
7759
+ };
7760
+ //#endregion
7613
7761
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7614
7762
  const walkInsideStatementBlocks = (node, visitor) => {
7615
7763
  if (!node || typeof node !== "object") return;
@@ -7761,6 +7909,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7761
7909
  };
7762
7910
  //#endregion
7763
7911
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
7912
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
7913
+ const RESOURCE_NOUN_BY_KIND = {
7914
+ subscribe: "subscription",
7915
+ timer: "timer",
7916
+ socket: "connection"
7917
+ };
7918
+ const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
7919
+ const isSubscribeOrObserveCall = (node) => {
7920
+ if (isSubscribeLikeCallExpression(node)) return true;
7921
+ return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
7922
+ };
7764
7923
  const findSubscribeLikeUsages = (callback) => {
7765
7924
  const usages = [];
7766
7925
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7772,6 +7931,14 @@ const findSubscribeLikeUsages = (callback) => {
7772
7931
  }
7773
7932
  walkAst(callback, (child) => {
7774
7933
  if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
7934
+ if (isSocketConstruction(child)) {
7935
+ usages.push({
7936
+ kind: "socket",
7937
+ node: child,
7938
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
7939
+ });
7940
+ return;
7941
+ }
7775
7942
  if (!isNodeOfType(child, "CallExpression")) return;
7776
7943
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7777
7944
  usages.push({
@@ -7781,7 +7948,7 @@ const findSubscribeLikeUsages = (callback) => {
7781
7948
  });
7782
7949
  return;
7783
7950
  }
7784
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
7951
+ if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
7785
7952
  kind: "subscribe",
7786
7953
  node: child,
7787
7954
  resourceName: child.callee.property.name
@@ -7797,6 +7964,13 @@ const collectCleanupBindings = (effectCallback) => {
7797
7964
  };
7798
7965
  if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
7799
7966
  if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
7967
+ walkAst(effectCallback.body, (child) => {
7968
+ if (!isSubscribeOrObserveCall(child)) return;
7969
+ if (!isNodeOfType(child, "CallExpression")) return;
7970
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
7971
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
7972
+ bindings.subscriptionNames.add(child.callee.object.name);
7973
+ });
7800
7974
  walkInsideStatementBlocks(effectCallback.body, (child) => {
7801
7975
  if (!isNodeOfType(child, "VariableDeclaration")) return;
7802
7976
  for (const declarator of child.declarations ?? []) {
@@ -7804,7 +7978,12 @@ const collectCleanupBindings = (effectCallback) => {
7804
7978
  const bindingName = declarator.id.name;
7805
7979
  bindings.effectScopeVariableNames.add(bindingName);
7806
7980
  const init = declarator.init;
7807
- if (!init || !isNodeOfType(init, "CallExpression")) continue;
7981
+ if (!init) continue;
7982
+ if (isSocketConstruction(init)) {
7983
+ bindings.subscriptionNames.add(bindingName);
7984
+ continue;
7985
+ }
7986
+ if (!isNodeOfType(init, "CallExpression")) continue;
7808
7987
  if (isSubscribeLikeCallExpression(init)) {
7809
7988
  bindings.subscriptionNames.add(bindingName);
7810
7989
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -7830,9 +8009,21 @@ const collectCleanupBindings = (effectCallback) => {
7830
8009
  });
7831
8010
  return bindings;
7832
8011
  };
7833
- const getRangeStart = (node) => {
7834
- const rangeStart = node.range?.[0];
7835
- return typeof rangeStart === "number" ? rangeStart : null;
8012
+ const removeSynchronouslyReleasedUsages = (callback, usages) => {
8013
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8014
+ if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8015
+ const releaseStarts = [];
8016
+ walkInsideStatementBlocks(callback.body, (child) => {
8017
+ if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8018
+ const releaseStart = getRangeStart(child);
8019
+ if (releaseStart !== null) releaseStarts.push(releaseStart);
8020
+ });
8021
+ if (releaseStarts.length === 0) return usages;
8022
+ return usages.filter((usage) => {
8023
+ const usageStart = getRangeStart(usage.node);
8024
+ if (usageStart === null) return true;
8025
+ return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8026
+ });
7836
8027
  };
7837
8028
  const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
7838
8029
  if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
@@ -7856,26 +8047,177 @@ const effectHasCleanupReturn = (callback, usages) => {
7856
8047
  });
7857
8048
  return didFindCleanupReturn;
7858
8049
  };
8050
+ const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8051
+ const isSelfReleasingListenerOptionProperty = (property) => {
8052
+ if (!isNodeOfType(property, "Property")) return false;
8053
+ const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
8054
+ if (keyName === "signal") return true;
8055
+ if (keyName !== "once") return false;
8056
+ return isNodeOfType(property.value, "Literal") && property.value.value === true;
8057
+ };
8058
+ const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
8059
+ const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8060
+ ["addEventListener", new Set(["removeEventListener", "abort"])],
8061
+ ["addListener", new Set([
8062
+ "removeListener",
8063
+ "off",
8064
+ "abort"
8065
+ ])],
8066
+ ["on", new Set([
8067
+ "off",
8068
+ "removeListener",
8069
+ "on"
8070
+ ])],
8071
+ ["subscribe", new Set(["unsubscribe", "unsub"])],
8072
+ ["sub", new Set(["unsub", "unsubscribe"])],
8073
+ ["watch", new Set(["unwatch", "close"])],
8074
+ ["listen", new Set(["unlisten", "close"])],
8075
+ [OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
8076
+ ]);
8077
+ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8078
+ "cleanup",
8079
+ "dispose",
8080
+ "destroy",
8081
+ "teardown"
8082
+ ]);
8083
+ const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8084
+ const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8085
+ const getReleaseVerbName = (node) => {
8086
+ if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8087
+ const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8088
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8089
+ const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8090
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
8091
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
8092
+ return null;
8093
+ };
8094
+ const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8095
+ const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8096
+ let didFindPairedRelease = false;
8097
+ walkAst(body, (child) => {
8098
+ if (didFindPairedRelease) return false;
8099
+ if (child !== body && isFunctionLike$1(child)) return false;
8100
+ const releaseVerbName = getReleaseVerbName(child);
8101
+ if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8102
+ didFindPairedRelease = true;
8103
+ return false;
8104
+ }
8105
+ });
8106
+ return didFindPairedRelease;
8107
+ };
8108
+ const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8109
+ const collectFileReleaseVerbNames = (anyNode) => {
8110
+ let programNode = anyNode;
8111
+ while (programNode.parent) programNode = programNode.parent;
8112
+ const cached = fileReleaseVerbNamesCache.get(programNode);
8113
+ if (cached) return cached;
8114
+ const releaseVerbNames = /* @__PURE__ */ new Set();
8115
+ walkAst(programNode, (child) => {
8116
+ const releaseVerbName = getReleaseVerbName(child);
8117
+ if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
8118
+ });
8119
+ fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8120
+ return releaseVerbNames;
8121
+ };
8122
+ const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8123
+ const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8124
+ const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8125
+ if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8126
+ for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
8127
+ return false;
8128
+ };
8129
+ const findRetainedFunctionLeak = (retainedFunction) => {
8130
+ if (!isFunctionLike$1(retainedFunction)) return null;
8131
+ const body = retainedFunction.body;
8132
+ if (!body) return null;
8133
+ const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8134
+ let leak = null;
8135
+ walkAst(body, (child) => {
8136
+ if (leak !== null) return false;
8137
+ if (isFunctionLike$1(child)) return false;
8138
+ if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8139
+ if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8140
+ leak = {
8141
+ kind: "socket",
8142
+ node: child,
8143
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8144
+ };
8145
+ return false;
8146
+ }
8147
+ if (!isNodeOfType(child, "CallExpression")) return;
8148
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8149
+ leak = {
8150
+ kind: "timer",
8151
+ node: child,
8152
+ resourceName: "setInterval"
8153
+ };
8154
+ return false;
8155
+ }
8156
+ if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8157
+ const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8158
+ if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
8159
+ kind: "subscribe",
8160
+ node: child,
8161
+ resourceName: registrationVerbName
8162
+ };
8163
+ return false;
8164
+ }
8165
+ });
8166
+ return leak;
8167
+ };
8168
+ const isRetainedComponentScopeFunction = (functionNode) => {
8169
+ if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
8170
+ if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
8171
+ if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8172
+ return enclosingComponentOrHookName(functionNode) !== null;
8173
+ };
7859
8174
  const effectNeedsCleanup = defineRule({
7860
8175
  id: "effect-needs-cleanup",
7861
8176
  title: "Effect subscription or timer never cleaned up",
7862
8177
  severity: "error",
7863
8178
  tags: ["test-noise"],
7864
- recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
7865
- create: (context) => ({ CallExpression(node) {
7866
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
7867
- const callback = getEffectCallback(node);
7868
- if (!callback) return;
7869
- const usages = findSubscribeLikeUsages(callback);
7870
- if (usages.length === 0) return;
7871
- if (effectHasCleanupReturn(callback, usages)) return;
7872
- const firstUsage = usages[0];
7873
- const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
7874
- context.report({
7875
- node,
7876
- message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
7877
- });
7878
- } })
8179
+ recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
8180
+ create: (context) => {
8181
+ const reportRetainedLeak = (retainedFunction) => {
8182
+ const leak = findRetainedFunctionLeak(retainedFunction);
8183
+ if (!leak) return;
8184
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8185
+ context.report({
8186
+ node: leak.node,
8187
+ message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
8188
+ });
8189
+ };
8190
+ return {
8191
+ CallExpression(node) {
8192
+ if (isHookCall$2(node, "useCallback")) {
8193
+ const retainedCallback = getEffectCallback(node);
8194
+ if (retainedCallback) reportRetainedLeak(retainedCallback);
8195
+ return;
8196
+ }
8197
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
8198
+ const callback = getEffectCallback(node);
8199
+ if (!callback) return;
8200
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
8201
+ if (usages.length === 0) return;
8202
+ if (effectHasCleanupReturn(callback, usages)) return;
8203
+ const firstUsage = usages[0];
8204
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
8205
+ context.report({
8206
+ node,
8207
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8208
+ });
8209
+ },
8210
+ FunctionDeclaration(node) {
8211
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8212
+ },
8213
+ ArrowFunctionExpression(node) {
8214
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8215
+ },
8216
+ FunctionExpression(node) {
8217
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8218
+ }
8219
+ };
8220
+ }
7879
8221
  });
7880
8222
  //#endregion
7881
8223
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8434,17 +8776,6 @@ const closureCaptures = (functionNode, scopes) => {
8434
8776
  return computedCaptures;
8435
8777
  };
8436
8778
  //#endregion
8437
- //#region src/plugin/utils/is-react-hook-name.ts
8438
- const isReactHookName = (name) => {
8439
- if (!name.startsWith("use")) return false;
8440
- if (name.length === 3) return true;
8441
- const fourthCharacter = name.charCodeAt(3);
8442
- return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8443
- };
8444
- //#endregion
8445
- //#region src/plugin/utils/is-react-component-or-hook-name.ts
8446
- const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8447
- //#endregion
8448
8779
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8449
8780
  const reactHocCalleeName = (callee) => {
8450
8781
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -8658,8 +8989,8 @@ const isAstDescendant = (inner, outer) => {
8658
8989
  * One cohesive concept: "given a captured symbol, is its value
8659
8990
  * structurally stable across re-renders (and therefore unnecessary
8660
8991
  * in a deps array)?". The rule reads `symbolHasStableValue` /
8661
- * `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
8662
- * `isRecursiveInitializerCapture` at multiple sites — extracting
8992
+ * `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
8993
+ * multiple sites — extracting
8663
8994
  * them lets the rule body stay focused on the diff-the-captured-vs-
8664
8995
  * declared logic.
8665
8996
  *
@@ -8715,11 +9046,6 @@ const symbolHasStableHookOrigin = (symbol) => {
8715
9046
  }
8716
9047
  return false;
8717
9048
  };
8718
- const symbolHasUseEffectEventOrigin = (symbol) => {
8719
- const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
8720
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
8721
- return getHookName(initializer.callee) === "useEffectEvent";
8722
- };
8723
9049
  const getFunctionValueNode = (symbol) => {
8724
9050
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
8725
9051
  const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
@@ -8805,6 +9131,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
8805
9131
  };
8806
9132
  const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
8807
9133
  //#endregion
9134
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
9135
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
9136
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
9137
+ if (importSource === null) return false;
9138
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
9139
+ };
9140
+ //#endregion
9141
+ //#region src/plugin/utils/is-non-react-effect-event-callee.ts
9142
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
9143
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
9144
+ return Boolean(symbol && symbol.kind !== "import");
9145
+ };
9146
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
9147
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
9148
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
9149
+ return false;
9150
+ };
9151
+ //#endregion
9152
+ //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9153
+ const getUseEffectEventCalleeName = (callee) => {
9154
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
9155
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9156
+ return null;
9157
+ };
9158
+ const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
9159
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
9160
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9161
+ if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
9162
+ return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9163
+ };
9164
+ //#endregion
8808
9165
  //#region src/plugin/rules/react-builtins/exhaustive-deps.ts
8809
9166
  const HOOKS_REQUIRING_DEPS_MATCH = new Set([
8810
9167
  "useEffect",
@@ -9455,7 +9812,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9455
9812
  if (isLiteralOrEmptyTemplate(stripped)) continue;
9456
9813
  if (isNodeOfType(stripped, "Identifier")) {
9457
9814
  const depSymbol = context.scopes.symbolFor(stripped);
9458
- if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
9815
+ if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
9459
9816
  context.report({
9460
9817
  node: elementNode,
9461
9818
  message: buildEffectEventDepMessage()
@@ -9616,7 +9973,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9616
9973
  });
9617
9974
  //#endregion
9618
9975
  //#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
9619
- const EMPTY_VISITORS$5 = {};
9976
+ const EMPTY_VISITORS$6 = {};
9620
9977
  const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
9621
9978
  const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
9622
9979
  const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
@@ -9628,7 +9985,7 @@ const expoNoNonInlinedEnv = defineRule({
9628
9985
  recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
9629
9986
  create: (context) => {
9630
9987
  const filename = normalizeFilename(context.filename ?? "");
9631
- if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
9988
+ if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
9632
9989
  return {
9633
9990
  MemberExpression(node) {
9634
9991
  if (!node.computed) return;
@@ -9881,7 +10238,10 @@ const PRAGMA = "React";
9881
10238
  const isReactFunctionCall = (node, expectedCall) => {
9882
10239
  if (!isNodeOfType(node, "CallExpression")) return false;
9883
10240
  if (getCalleeName$2(node) !== expectedCall) return false;
9884
- if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10241
+ if (isNodeOfType(node.callee, "MemberExpression")) {
10242
+ const receiver = stripParenExpression(node.callee.object);
10243
+ return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10244
+ }
9885
10245
  return true;
9886
10246
  };
9887
10247
  //#endregion
@@ -11046,8 +11406,8 @@ const interactiveSupportsFocus = defineRule({
11046
11406
  if (node.attributes.length === 0) return;
11047
11407
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11048
11408
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11049
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11050
- if (!role) return;
11409
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
11410
+ if (roleCandidates === null || roleCandidates.length === 0) return;
11051
11411
  let hasInteractiveHandler = false;
11052
11412
  for (const attribute of node.attributes) {
11053
11413
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11061,11 +11421,16 @@ const interactiveSupportsFocus = defineRule({
11061
11421
  const elementType = getElementType(node, context.settings);
11062
11422
  if (!HTML_TAGS.has(elementType)) return;
11063
11423
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
11064
- if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11065
- if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
11066
11424
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
11067
- if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11068
- const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
11425
+ const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
11426
+ for (const role of roleCandidates) {
11427
+ if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11428
+ if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
11429
+ if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11430
+ }
11431
+ const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
11432
+ const roleDisplay = roleCandidates.join("' / '");
11433
+ const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
11069
11434
  context.report({
11070
11435
  node,
11071
11436
  message
@@ -11254,16 +11619,6 @@ const collectHandlerReferencedNames = (root) => {
11254
11619
  return names;
11255
11620
  };
11256
11621
  //#endregion
11257
- //#region src/plugin/utils/find-enclosing-function.ts
11258
- const findEnclosingFunction = (node) => {
11259
- let cursor = node.parent;
11260
- while (cursor) {
11261
- if (isFunctionLike$1(cursor)) return cursor;
11262
- cursor = cursor.parent ?? null;
11263
- }
11264
- return null;
11265
- };
11266
- //#endregion
11267
11622
  //#region src/plugin/utils/get-function-binding-name.ts
11268
11623
  const getFunctionBindingIdentifier = (functionNode) => {
11269
11624
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -11963,14 +12318,15 @@ const jsCacheStorage = defineRule({
11963
12318
  "ArrowFunctionExpression:exit": exitFunctionScope,
11964
12319
  CallExpression(node) {
11965
12320
  if (!isMemberProperty(node.callee, "getItem")) return;
11966
- if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
12321
+ const receiver = stripParenExpression(node.callee.object);
12322
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
11967
12323
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
11968
12324
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
11969
12325
  const storageKey = String(node.arguments[0].value);
11970
12326
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
11971
12327
  storageReadCounts.set(storageKey, readCount);
11972
12328
  if (readCount === 2) {
11973
- const storageName = node.callee.object.name;
12329
+ const storageName = receiver.name;
11974
12330
  context.report({
11975
12331
  node,
11976
12332
  message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
@@ -11984,13 +12340,16 @@ const jsCacheStorage = defineRule({
11984
12340
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
11985
12341
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
11986
12342
  const callee = callExpression.callee;
11987
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
11988
- if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
11989
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
11990
- const receiver = callee.object;
11991
- if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
11992
- return true;
12343
+ if (isNodeOfType(callee, "MemberExpression")) {
12344
+ const receiver = stripParenExpression(callee.object);
12345
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12346
+ if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12347
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12348
+ return true;
12349
+ }
12350
+ return false;
11993
12351
  }
12352
+ if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
11994
12353
  return false;
11995
12354
  };
11996
12355
  const isChainPassThroughCall = (callExpression) => {
@@ -12002,10 +12361,7 @@ const isChainPassThroughCall = (callExpression) => {
12002
12361
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12003
12362
  let cursor = receiverNode;
12004
12363
  while (cursor) {
12005
- if (isNodeOfType(cursor, "ChainExpression")) {
12006
- cursor = cursor.expression;
12007
- continue;
12008
- }
12364
+ cursor = stripParenExpression(cursor);
12009
12365
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12010
12366
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12011
12367
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12081,10 +12437,7 @@ const isStringSplitRootedChain = (receiverNode) => {
12081
12437
  let hops = 0;
12082
12438
  while (cursor && hops < 12) {
12083
12439
  hops += 1;
12084
- if (isNodeOfType(cursor, "ChainExpression")) {
12085
- cursor = cursor.expression;
12086
- continue;
12087
- }
12440
+ cursor = stripParenExpression(cursor);
12088
12441
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12089
12442
  const callee = cursor.callee;
12090
12443
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12108,10 +12461,7 @@ const isSmallLiteralArray = (node) => {
12108
12461
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12109
12462
  let cursor = receiverNode;
12110
12463
  while (cursor) {
12111
- if (isNodeOfType(cursor, "ChainExpression")) {
12112
- cursor = cursor.expression;
12113
- continue;
12114
- }
12464
+ cursor = stripParenExpression(cursor);
12115
12465
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12116
12466
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12117
12467
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12176,7 +12526,7 @@ const jsCombineIterations = defineRule({
12176
12526
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12177
12527
  const outerMethod = node.callee.property.name;
12178
12528
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12179
- const innerCall = node.callee.object;
12529
+ const innerCall = stripParenExpression(node.callee.object);
12180
12530
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12181
12531
  const innerMethod = innerCall.callee.property.name;
12182
12532
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12249,10 +12599,10 @@ const jsFlatmapFilter = defineRule({
12249
12599
  if (!filterArgument) return;
12250
12600
  const isIdentityArrow = isNodeOfType(filterArgument, "ArrowFunctionExpression") && filterArgument.params?.length === 1 && isNodeOfType(filterArgument.body, "Identifier") && isNodeOfType(filterArgument.params[0], "Identifier") && filterArgument.body.name === filterArgument.params[0].name;
12251
12601
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12252
- const innerCall = node.callee.object;
12602
+ const innerCall = stripParenExpression(node.callee.object);
12253
12603
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12254
12604
  if (innerCall.callee.property.name !== "map") return;
12255
- const receiver = innerCall.callee.object;
12605
+ const receiver = stripParenExpression(innerCall.callee.object);
12256
12606
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12257
12607
  const elements = receiver.elements ?? [];
12258
12608
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13512,7 +13862,7 @@ const jsTosortedImmutable = defineRule({
13512
13862
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13513
13863
  create: (context) => ({ CallExpression(node) {
13514
13864
  if (!isMemberProperty(node.callee, "sort")) return;
13515
- const receiver = node.callee.object;
13865
+ const receiver = stripParenExpression(node.callee.object);
13516
13866
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13517
13867
  const spreadArgument = receiver.elements[0].argument;
13518
13868
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18549,7 +18899,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18549
18899
  };
18550
18900
  const describeClientSideNavigation = (node) => {
18551
18901
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18552
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
18902
+ const receiver = stripParenExpression(node.callee.object);
18903
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18553
18904
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18554
18905
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18555
18906
  }
@@ -19169,7 +19520,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19169
19520
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19170
19521
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19171
19522
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19172
- if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
19523
+ if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19173
19524
  return node.callee.property.name;
19174
19525
  };
19175
19526
  const isMutatingFetchCall = (node) => {
@@ -19183,7 +19534,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19183
19534
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19184
19535
  const { property, object } = node.callee;
19185
19536
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19186
- if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
19537
+ if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19187
19538
  return true;
19188
19539
  };
19189
19540
  const getDbCallDescription = (node) => {
@@ -19191,7 +19542,8 @@ const getDbCallDescription = (node) => {
19191
19542
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19192
19543
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19193
19544
  const methodName = node.callee.property.name;
19194
- const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19545
+ const receiver = stripParenExpression(node.callee.object);
19546
+ const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19195
19547
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19196
19548
  };
19197
19549
  const findSideEffect = (node, options = {}) => {
@@ -20591,13 +20943,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20591
20943
  if (isNodeOfType(current, "CallExpression")) {
20592
20944
  let node = ref.identifier;
20593
20945
  let parent = node.parent;
20594
- while (parent && isNodeOfType(parent, "MemberExpression")) {
20946
+ while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20595
20947
  node = parent;
20596
20948
  parent = node.parent;
20597
20949
  }
20598
20950
  if (current.callee === node) return current;
20599
20951
  }
20600
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
20952
+ if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20601
20953
  return null;
20602
20954
  };
20603
20955
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -20732,7 +21084,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
20732
21084
  const importDeclaration = declarationNode.parent;
20733
21085
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
20734
21086
  }));
20735
- const isHookCallee = (analysis, node, hookName) => {
21087
+ const isHookCallee$1 = (analysis, node, hookName) => {
20736
21088
  if (!node) return false;
20737
21089
  if (isNodeOfType(node, "Identifier")) {
20738
21090
  if (node.name === hookName) return true;
@@ -20741,15 +21093,19 @@ const isHookCallee = (analysis, node, hookName) => {
20741
21093
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
20742
21094
  return false;
20743
21095
  }
20744
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21096
+ if (isNodeOfType(node, "MemberExpression")) {
21097
+ const receiver = stripParenExpression(node.object);
21098
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21099
+ }
20745
21100
  return false;
20746
21101
  };
20747
21102
  const isUseEffect = (node) => {
20748
21103
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
20749
21104
  const callee = node.callee;
20750
21105
  if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
20751
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
20752
- return false;
21106
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
21107
+ const receiver = stripParenExpression(callee.object);
21108
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
20753
21109
  };
20754
21110
  const getEffectFn = (analysis, node) => {
20755
21111
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -20777,7 +21133,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20777
21133
  const node = def.node;
20778
21134
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20779
21135
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20780
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21136
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20781
21137
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20782
21138
  const elements = node.id.elements ?? [];
20783
21139
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -20788,7 +21144,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
20788
21144
  const node = def.node;
20789
21145
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20790
21146
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20791
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21147
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20792
21148
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20793
21149
  const elements = node.id.elements ?? [];
20794
21150
  if (elements.length !== 2) return false;
@@ -20835,7 +21191,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20835
21191
  const node = def.node;
20836
21192
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20837
21193
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20838
- return isHookCallee(analysis, node.init.callee, "useRef");
21194
+ return isHookCallee$1(analysis, node.init.callee, "useRef");
20839
21195
  }));
20840
21196
  const isRefCurrent = (ref) => {
20841
21197
  const parent = ref.identifier.parent;
@@ -20848,11 +21204,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
20848
21204
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
20849
21205
  const isPropCallbackInvocationRef = (analysis, ref) => {
20850
21206
  if (!isPropAlias(analysis, ref)) return false;
20851
- const identifier = ref.identifier;
20852
- const parent = identifier.parent;
21207
+ let effectiveNode = ref.identifier;
21208
+ let parent = effectiveNode.parent;
21209
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21210
+ effectiveNode = parent;
21211
+ parent = effectiveNode.parent;
21212
+ }
20853
21213
  if (!parent) return false;
20854
- if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
20855
- if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21214
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21215
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
20856
21216
  const memberParent = parent.parent;
20857
21217
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
20858
21218
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -20863,7 +21223,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
20863
21223
  };
20864
21224
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
20865
21225
  const getUseStateDecl = (analysis, ref) => {
20866
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21226
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
20867
21227
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
20868
21228
  return node ?? null;
20869
21229
  };
@@ -21068,7 +21428,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21068
21428
  const noAdjustStateOnPropChange = defineRule({
21069
21429
  id: "no-adjust-state-on-prop-change",
21070
21430
  title: "State synced to a prop inside an effect",
21071
- severity: "error",
21431
+ severity: "warn",
21072
21432
  tags: ["test-noise"],
21073
21433
  recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
21074
21434
  create: (context) => ({ CallExpression(node) {
@@ -21109,7 +21469,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21109
21469
  "summary",
21110
21470
  "textarea"
21111
21471
  ]);
21472
+ const isStaticallyFalseBooleanAttribute = (attribute) => {
21473
+ const value = attribute.value;
21474
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
21475
+ const expression = value.expression;
21476
+ return isNodeOfType(expression, "Literal") && expression.value === false;
21477
+ };
21478
+ const DISABLEABLE_TAGS = new Set([
21479
+ "button",
21480
+ "input",
21481
+ "select",
21482
+ "textarea"
21483
+ ]);
21112
21484
  const isNativelyFocusable = (tagName, openingElement) => {
21485
+ if (DISABLEABLE_TAGS.has(tagName)) {
21486
+ const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
21487
+ if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
21488
+ }
21113
21489
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21114
21490
  switch (tagName) {
21115
21491
  case "input": {
@@ -21123,7 +21499,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
21123
21499
  case "a":
21124
21500
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21125
21501
  case "audio":
21126
- case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
21502
+ case "video": {
21503
+ const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
21504
+ return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
21505
+ }
21127
21506
  default: return false;
21128
21507
  }
21129
21508
  };
@@ -22087,7 +22466,8 @@ const SECOND_INDEX_METHODS = new Set([
22087
22466
  "some"
22088
22467
  ]);
22089
22468
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22090
- const isPositionallyStableIterationReceiver = (receiver) => {
22469
+ const isPositionallyStableIterationReceiver = (receiverNode) => {
22470
+ const receiver = stripParenExpression(receiverNode);
22091
22471
  if (isAllLiteralArrayExpression(receiver)) return true;
22092
22472
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22093
22473
  const only = receiver.elements[0];
@@ -22098,17 +22478,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
22098
22478
  }
22099
22479
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22100
22480
  const callee = receiver.callee;
22101
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from" && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22481
+ if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22102
22482
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22103
22483
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22104
22484
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22105
22485
  return false;
22106
22486
  };
22107
- const isArrayFromMapperCallback = (parentCall, callback) => {
22108
- if (parentCall.arguments[1] !== callback) return false;
22109
- const callee = parentCall.callee;
22110
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
22111
- };
22487
+ const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22112
22488
  const isArrayFromSourcePositionallyStable = (source) => {
22113
22489
  if (isNodeOfType(source, "ObjectExpression")) {
22114
22490
  for (const property of source.properties ?? []) {
@@ -22167,18 +22543,12 @@ const expressionUsesIndex = (expression, paramName) => {
22167
22543
  return false;
22168
22544
  }
22169
22545
  if (isNodeOfType(expression, "CallExpression")) {
22170
- if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
22546
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22171
22547
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22172
22548
  }
22173
22549
  return false;
22174
22550
  };
22175
- const isReactCloneElement = (callExpression) => {
22176
- const callee = callExpression.callee;
22177
- if (!isNodeOfType(callee, "MemberExpression")) return false;
22178
- if (!isNodeOfType(callee.property, "Identifier")) return false;
22179
- if (callee.property.name !== "cloneElement") return false;
22180
- return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
22181
- };
22551
+ const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22182
22552
  const noArrayIndexKey = defineRule({
22183
22553
  id: "no-array-index-key",
22184
22554
  title: "Array index used as a key",
@@ -22477,6 +22847,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
22477
22847
  const section = manifest[sectionName];
22478
22848
  return typeof section === "object" && section !== null && Object.keys(section).length > 0;
22479
22849
  });
22850
+ const declaresDependency = (manifest, dependencyName) => {
22851
+ for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
22852
+ return false;
22853
+ };
22480
22854
  const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
22481
22855
  const classifyPackagePlatform = (filename) => {
22482
22856
  const manifest = readNearestPackageManifest(filename);
@@ -22493,9 +22867,7 @@ const classifyPackagePlatform = (filename) => {
22493
22867
  return result;
22494
22868
  };
22495
22869
  //#endregion
22496
- //#region src/plugin/utils/is-react-native-file.ts
22497
- const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22498
- const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22870
+ //#region src/plugin/utils/is-package-nested-below-project-root.ts
22499
22871
  const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
22500
22872
  const resolveRealDirectory = (directory) => {
22501
22873
  const cached = cachedRealDirectoryByDirectory.get(directory);
@@ -22516,6 +22888,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
22516
22888
  const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
22517
22889
  return realPackageDirectory.startsWith(rootPrefix);
22518
22890
  };
22891
+ //#endregion
22892
+ //#region src/plugin/utils/is-react-native-file.ts
22893
+ const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22894
+ const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22519
22895
  const classifyReactNativeFileTarget = (context) => {
22520
22896
  const rawFilename = context.filename;
22521
22897
  if (!rawFilename) return "unknown";
@@ -22896,7 +23272,7 @@ const isAsyncFunctionLike = (node) => {
22896
23272
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
22897
23273
  return false;
22898
23274
  };
22899
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23275
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
22900
23276
  "forEach",
22901
23277
  "map",
22902
23278
  "filter",
@@ -22917,31 +23293,51 @@ const runsOnEffectDispatch = (functionNode) => {
22917
23293
  if (parent.callee === functionNode) return true;
22918
23294
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
22919
23295
  const callee = parent.callee;
22920
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
23296
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
22921
23297
  };
22922
23298
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
22923
- const isGuardWithTerminatingBranch = (statement) => {
22924
- if (!isNodeOfType(statement, "IfStatement")) return null;
22925
- if (statement.alternate) return null;
22926
- const consequent = statement.consequent;
22927
- if (isTerminatingStatement(consequent)) return consequent;
22928
- if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
22929
- return null;
22930
- };
22931
- const countStatementSequenceSetStateCalls = (statements, context) => {
23299
+ const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
23300
+ const analyzeStatementSequence = (statements, context) => {
22932
23301
  let fallThroughCount = 0;
22933
- let maxTerminatingPathCount = 0;
23302
+ let maxTerminatedCount = 0;
22934
23303
  for (const statement of statements) {
22935
- if (isFunctionLike$1(statement)) continue;
22936
- const guardBranch = isGuardWithTerminatingBranch(statement);
22937
- if (guardBranch) {
22938
- maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
23304
+ if (isNodeOfType(statement, "IfStatement")) {
23305
+ fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
23306
+ const thenSummary = analyzeBranchStatements(statement.consequent, context);
23307
+ const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
23308
+ fallThroughCount: 0,
23309
+ maxTerminatedCount: 0,
23310
+ doAllPathsTerminate: false
23311
+ };
23312
+ maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
23313
+ if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
23314
+ fallThroughCount: 0,
23315
+ maxTerminatedCount,
23316
+ doAllPathsTerminate: true
23317
+ };
23318
+ const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
23319
+ fallThroughCount += Math.max(...fallThroughBranchCounts);
22939
23320
  continue;
22940
23321
  }
22941
- if (isTerminatingStatement(statement)) break;
23322
+ if (isTerminatingStatement(statement)) {
23323
+ const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
23324
+ return {
23325
+ fallThroughCount: 0,
23326
+ maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
23327
+ doAllPathsTerminate: true
23328
+ };
23329
+ }
22942
23330
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
22943
23331
  }
22944
- return Math.max(maxTerminatingPathCount, fallThroughCount);
23332
+ return {
23333
+ fallThroughCount,
23334
+ maxTerminatedCount,
23335
+ doAllPathsTerminate: false
23336
+ };
23337
+ };
23338
+ const countStatementSequenceSetStateCalls = (statements, context) => {
23339
+ const summary = analyzeStatementSequence(statements, context);
23340
+ return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
22945
23341
  };
22946
23342
  const collectLocalHelperFunctions = (root) => {
22947
23343
  const helpersByName = /* @__PURE__ */ new Map();
@@ -22968,14 +23364,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
22968
23364
  if (!isNodeOfType(parent, "ExpressionStatement")) return false;
22969
23365
  return parent.parent === effectCallback.body;
22970
23366
  };
23367
+ const countFunctionBodySetStateCalls = (functionNode, context) => {
23368
+ if (isAsyncFunctionLike(functionNode)) return 0;
23369
+ const body = functionNode.body;
23370
+ if (!body) return 0;
23371
+ return countMaxPathSetStateCalls(body, context);
23372
+ };
22971
23373
  const countMaxPathSetStateCalls = (node, context) => {
22972
23374
  if (!node || typeof node !== "object") return 0;
22973
- if (isAsyncFunctionLike(node)) return 0;
23375
+ if (isFunctionLike$1(node)) return 0;
22974
23376
  if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
22975
23377
  if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
22976
23378
  const consequent = node.consequent;
22977
23379
  const alternate = node.alternate;
22978
- return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
23380
+ const testCount = countMaxPathSetStateCalls(node.test, context);
23381
+ const thenCount = countMaxPathSetStateCalls(consequent, context);
23382
+ const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
23383
+ return testCount + Math.max(thenCount, elseCount);
22979
23384
  }
22980
23385
  if (isNodeOfType(node, "SwitchStatement")) {
22981
23386
  let maxRunSetters = 0;
@@ -23005,28 +23410,31 @@ const countMaxPathSetStateCalls = (node, context) => {
23005
23410
  }
23006
23411
  if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
23007
23412
  let nestedSettersInArgs = 0;
23008
- for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
23413
+ for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
23009
23414
  return 1 + nestedSettersInArgs;
23010
23415
  }
23011
23416
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
23012
23417
  const helperFunction = context.helpersByName.get(node.callee.name);
23013
23418
  if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
23014
23419
  context.activeHelpers.add(helperFunction);
23015
- let helperCount = countMaxPathSetStateCalls(helperFunction, context);
23420
+ let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
23016
23421
  context.activeHelpers.delete(helperFunction);
23017
23422
  for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
23018
23423
  return helperCount;
23019
23424
  }
23020
23425
  }
23021
- const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
23426
+ const countChild = (child) => {
23427
+ if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
23428
+ return countMaxPathSetStateCalls(child, context);
23429
+ };
23022
23430
  let total = 0;
23023
23431
  const nodeRecord = node;
23024
23432
  for (const key of Object.keys(nodeRecord)) {
23025
23433
  if (key === "parent") continue;
23026
23434
  const child = nodeRecord[key];
23027
23435
  if (Array.isArray(child)) {
23028
- for (const item of child) if (item && typeof item === "object" && "type" in item && shouldWalkChild(item)) total += countMaxPathSetStateCalls(item, context);
23029
- } else if (child && typeof child === "object" && "type" in child && shouldWalkChild(child)) total += countMaxPathSetStateCalls(child, context);
23436
+ for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
23437
+ } else if (child && typeof child === "object" && "type" in child) total += countChild(child);
23030
23438
  }
23031
23439
  return total;
23032
23440
  };
@@ -23060,7 +23468,9 @@ const isDevOnlyGuardedEffect = (callback) => {
23060
23468
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23061
23469
  const firstStatement = (body.body ?? [])[0];
23062
23470
  if (!firstStatement) return false;
23063
- if (!isGuardWithTerminatingBranch(firstStatement)) return false;
23471
+ if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
23472
+ const consequent = firstStatement.consequent;
23473
+ if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23064
23474
  return mentionsDevEnvFlag(firstStatement.test);
23065
23475
  };
23066
23476
  const noCascadingSetState = defineRule({
@@ -23075,7 +23485,7 @@ const noCascadingSetState = defineRule({
23075
23485
  const callback = getEffectCallback(node);
23076
23486
  if (!callback) return;
23077
23487
  if (isDevOnlyGuardedEffect(callback)) return;
23078
- const setStateCallCount = countMaxPathSetStateCalls(callback, {
23488
+ const setStateCallCount = countFunctionBodySetStateCalls(callback, {
23079
23489
  helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
23080
23490
  activeHelpers: /* @__PURE__ */ new Set(),
23081
23491
  effectCallback: callback
@@ -23419,40 +23829,6 @@ const noCloneElement = defineRule({
23419
23829
  } })
23420
23830
  });
23421
23831
  //#endregion
23422
- //#region src/plugin/utils/component-or-hook-display-name.ts
23423
- const hocWrapperCalleeName = (callee) => {
23424
- if (isNodeOfType(callee, "Identifier")) return callee.name;
23425
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
23426
- return null;
23427
- };
23428
- const displayNameFromFunctionBinding = (functionNode) => {
23429
- let current = functionNode;
23430
- for (;;) {
23431
- const parent = current.parent;
23432
- if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
23433
- const calleeName = hocWrapperCalleeName(parent.callee);
23434
- if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
23435
- current = parent;
23436
- continue;
23437
- }
23438
- }
23439
- break;
23440
- }
23441
- const binding = current.parent;
23442
- if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
23443
- return null;
23444
- };
23445
- const componentOrHookDisplayNameForFunction = (functionNode) => {
23446
- if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
23447
- return displayNameFromFunctionBinding(functionNode);
23448
- };
23449
- //#endregion
23450
- //#region src/plugin/utils/enclosing-component-or-hook-name.ts
23451
- const enclosingComponentOrHookName = (node) => {
23452
- const functionNode = findEnclosingFunction(node);
23453
- return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
23454
- };
23455
- //#endregion
23456
23832
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23457
23833
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23458
23834
  const CONTEXT_MODULES = [
@@ -24423,11 +24799,21 @@ const isInitialOnlySetterCall = (callExpr) => {
24423
24799
  return isInitialOnlyPropName(arg.name);
24424
24800
  };
24425
24801
  //#endregion
24802
+ //#region src/plugin/utils/is-no-op-statement.ts
24803
+ const isNoOpStatement = (statement) => {
24804
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
24805
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24806
+ const expression = stripParenExpression(statement.expression);
24807
+ if (isNodeOfType(expression, "Literal")) return true;
24808
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
24809
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
24810
+ return false;
24811
+ };
24812
+ //#endregion
24426
24813
  //#region src/plugin/utils/get-callback-statements.ts
24427
24814
  const getCallbackStatements = (callback) => {
24428
24815
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
24429
- if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
24430
- return callback.body ? [callback.body] : [];
24816
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24431
24817
  };
24432
24818
  //#endregion
24433
24819
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24466,6 +24852,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24466
24852
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24467
24853
  }
24468
24854
  };
24855
+ const flattenGuardedStatements = (statements) => {
24856
+ const flattened = [];
24857
+ for (const statement of statements) {
24858
+ if (isNoOpStatement(statement)) continue;
24859
+ if (isNodeOfType(statement, "ExpressionStatement")) {
24860
+ flattened.push(statement);
24861
+ continue;
24862
+ }
24863
+ if (isNodeOfType(statement, "IfStatement")) {
24864
+ for (const branch of [statement.consequent, statement.alternate]) {
24865
+ if (!branch) continue;
24866
+ const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
24867
+ if (flattenedBranch === null) return null;
24868
+ flattened.push(...flattenedBranch);
24869
+ }
24870
+ continue;
24871
+ }
24872
+ return null;
24873
+ }
24874
+ return flattened;
24875
+ };
24469
24876
  const noDerivedStateEffect = defineRule({
24470
24877
  id: "no-derived-state-effect",
24471
24878
  title: "Derived state stored in an effect",
@@ -24497,8 +24904,8 @@ const noDerivedStateEffect = defineRule({
24497
24904
  }
24498
24905
  }
24499
24906
  if (sawAnyDep && allDepsAreInitialOnly) return;
24500
- const statements = getCallbackStatements(callback);
24501
- if (statements.length === 0) return;
24907
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
24908
+ if (statements === null || statements.length === 0) return;
24502
24909
  if (!statements.every((statement) => {
24503
24910
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24504
24911
  const expression = statement.expression;
@@ -25133,7 +25540,7 @@ const noDidMountSetState = defineRule({
25133
25540
  const { mode } = resolveSettings$20(context.settings);
25134
25541
  return { CallExpression(node) {
25135
25542
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25136
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25543
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25137
25544
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25138
25545
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25139
25546
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25258,7 +25665,7 @@ const noDidUpdateSetState = defineRule({
25258
25665
  const { mode } = resolveSettings$19(context.settings);
25259
25666
  return { CallExpression(node) {
25260
25667
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25261
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25668
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25262
25669
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25263
25670
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25264
25671
  if (isInsideDiffGuard(node)) return;
@@ -25355,18 +25762,45 @@ const collectUseStateBindings = (componentBody) => {
25355
25762
  //#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
25356
25763
  const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
25357
25764
  const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
25765
+ const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
25766
+ const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
25767
+ "assign",
25768
+ "entries",
25769
+ "fromEntries",
25770
+ "keys",
25771
+ "values"
25772
+ ]);
25773
+ const ARRAY_COPY_METHOD_NAMES = new Set([
25774
+ "map",
25775
+ "filter",
25776
+ "slice",
25777
+ "concat",
25778
+ "flat",
25779
+ "flatMap",
25780
+ "toSorted",
25781
+ "toReversed",
25782
+ "toSpliced",
25783
+ "with"
25784
+ ]);
25358
25785
  const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
25359
25786
  const isPlainDataProducerCall = (expression) => {
25360
25787
  if (!isNodeOfType(expression, "CallExpression")) return false;
25361
25788
  const callee = expression.callee;
25362
25789
  if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
25363
25790
  if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
25364
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25791
+ if (isNodeOfType(callee.object, "Identifier")) {
25792
+ if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25793
+ if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
25794
+ if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
25795
+ }
25365
25796
  return producesPlainStateValue(callee.object);
25366
25797
  };
25798
+ const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
25799
+ const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
25367
25800
  const producesPlainStateValue = (expression) => {
25368
25801
  const unwrapped = stripParenExpression(expression);
25369
25802
  if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
25803
+ if (isPlainDataNewExpression(unwrapped)) return true;
25370
25804
  if (isNullOrUndefinedExpression(unwrapped)) return true;
25371
25805
  if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
25372
25806
  return isPlainDataProducerCall(unwrapped);
@@ -25381,6 +25815,38 @@ const initializerMarksPlainState = (initializerArgument) => {
25381
25815
  }
25382
25816
  return producesPlainStateValue(unwrapped);
25383
25817
  };
25818
+ const producesOpaqueInstanceValue = (expression) => {
25819
+ if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
25820
+ if (!isNodeOfType(expression, "CallExpression")) return false;
25821
+ const callee = expression.callee;
25822
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
25823
+ if (isPlainDataProducerCall(expression)) return false;
25824
+ if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
25825
+ return true;
25826
+ };
25827
+ const collectSetterValueObservations = (componentBody, setterNames) => {
25828
+ const plainFedSetterNames = /* @__PURE__ */ new Set();
25829
+ const opaqueFedSetterNames = /* @__PURE__ */ new Set();
25830
+ walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
25831
+ if (!isNodeOfType(node, "CallExpression")) return;
25832
+ if (!isNodeOfType(node.callee, "Identifier")) return;
25833
+ const setterName = node.callee.name;
25834
+ if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
25835
+ const argument = node.arguments?.[0];
25836
+ if (!argument) return;
25837
+ const unwrapped = stripParenExpression(argument);
25838
+ if (isNullOrUndefinedExpression(unwrapped)) return;
25839
+ if (producesPlainStateValue(unwrapped)) {
25840
+ plainFedSetterNames.add(setterName);
25841
+ return;
25842
+ }
25843
+ if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
25844
+ }, true);
25845
+ return {
25846
+ plainFedSetterNames,
25847
+ opaqueFedSetterNames
25848
+ };
25849
+ };
25384
25850
  const collectCallbackRefSetterNames = (componentBody) => {
25385
25851
  const callbackRefSetterNames = /* @__PURE__ */ new Set();
25386
25852
  walkAst(componentBody, (node) => {
@@ -25450,11 +25916,15 @@ const noDirectStateMutation = defineRule({
25450
25916
  if (bindings.length === 0) return;
25451
25917
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
25452
25918
  const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
25919
+ const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
25453
25920
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
25454
25921
  for (const binding of bindings) {
25455
25922
  if (callbackRefSetterNames.has(binding.setterName)) continue;
25456
25923
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
25457
- if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
25924
+ const initializerArgument = binding.declarator.init.arguments?.[0];
25925
+ if (!initializerMarksPlainState(initializerArgument)) continue;
25926
+ if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
25927
+ plainObjectStateValueNames.add(binding.valueName);
25458
25928
  }
25459
25929
  const visitMutationCandidate = (child, currentlyShadowed) => {
25460
25930
  if (isNodeOfType(child, "AssignmentExpression")) {
@@ -25579,9 +26049,10 @@ const noDocumentStartViewTransition = defineRule({
25579
26049
  create: (context) => ({ CallExpression(node) {
25580
26050
  const callee = node.callee;
25581
26051
  if (!isNodeOfType(callee, "MemberExpression")) return;
25582
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26052
+ const receiver = stripParenExpression(callee.object);
26053
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25583
26054
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
25584
- if (context.scopes.symbolFor(callee.object) !== null) return;
26055
+ if (context.scopes.symbolFor(receiver) !== null) return;
25585
26056
  if (!importsReactViewTransition(node)) return;
25586
26057
  context.report({
25587
26058
  node,
@@ -25601,7 +26072,8 @@ const noDocumentWrite = defineRule({
25601
26072
  create: (context) => ({ CallExpression(node) {
25602
26073
  const callee = node.callee;
25603
26074
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
25604
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26075
+ const receiver = stripParenExpression(callee.object);
26076
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25605
26077
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
25606
26078
  context.report({
25607
26079
  node,
@@ -26233,13 +26705,6 @@ const noEffectEventHandler = defineRule({
26233
26705
  }
26234
26706
  });
26235
26707
  //#endregion
26236
- //#region src/plugin/utils/is-imported-from-non-react-module.ts
26237
- const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
26238
- const importSource = getImportSourceForName(contextNode, localIdentifierName);
26239
- if (importSource === null) return false;
26240
- return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
26241
- };
26242
- //#endregion
26243
26708
  //#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
26244
26709
  const createComponentBindingStackTracker = (callbacks) => {
26245
26710
  const componentBindingStack = [];
@@ -26293,7 +26758,7 @@ const noEffectEventInDeps = defineRule({
26293
26758
  const initializer = declaratorNode.init;
26294
26759
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26295
26760
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26296
- if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26761
+ if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
26297
26762
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26298
26763
  } });
26299
26764
  return {
@@ -28873,7 +29338,7 @@ const noIsMounted = defineRule({
28873
29338
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
28874
29339
  create: (context) => ({ CallExpression(node) {
28875
29340
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
28876
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
29341
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
28877
29342
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
28878
29343
  if (!getParentComponent(node)) return;
28879
29344
  context.report({
@@ -28888,7 +29353,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
28888
29353
  const isJsonMethodCall = (node, method) => {
28889
29354
  if (!isNodeOfType(node, "CallExpression")) return false;
28890
29355
  const callee = node.callee;
28891
- return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
29356
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
29357
+ const receiver = stripParenExpression(callee.object);
29358
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
28892
29359
  };
28893
29360
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
28894
29361
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -28973,7 +29440,7 @@ const extractReturnTypeAnnotation = (returnType) => {
28973
29440
  const noJsxElementType = defineRule({
28974
29441
  id: "no-jsx-element-type",
28975
29442
  title: "No JSX.Element",
28976
- severity: "error",
29443
+ severity: "warn",
28977
29444
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
28978
29445
  create: (context) => {
28979
29446
  let isJsxImported = false;
@@ -29299,6 +29766,378 @@ const noLegacyContextApi = defineRule({
29299
29766
  }
29300
29767
  });
29301
29768
  //#endregion
29769
+ //#region src/plugin/utils/executes-during-render.ts
29770
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29771
+ "map",
29772
+ "filter",
29773
+ "forEach",
29774
+ "flatMap",
29775
+ "reduce",
29776
+ "reduceRight",
29777
+ "some",
29778
+ "every",
29779
+ "find",
29780
+ "findIndex",
29781
+ "findLast",
29782
+ "findLastIndex",
29783
+ "sort",
29784
+ "toSorted"
29785
+ ]);
29786
+ const executesDuringRender = (functionNode) => {
29787
+ const parent = functionNode.parent;
29788
+ if (!isNodeOfType(parent, "CallExpression")) return false;
29789
+ if (parent.callee === functionNode) return true;
29790
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
29791
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
29792
+ };
29793
+ //#endregion
29794
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29795
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
29796
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29797
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29798
+ return false;
29799
+ };
29800
+ //#endregion
29801
+ //#region src/plugin/utils/find-declarator-for-binding.ts
29802
+ const findDeclaratorForBinding = (bindingIdentifier) => {
29803
+ let ancestor = bindingIdentifier.parent;
29804
+ while (ancestor) {
29805
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29806
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29807
+ ancestor = ancestor.parent ?? null;
29808
+ }
29809
+ return null;
29810
+ };
29811
+ //#endregion
29812
+ //#region src/plugin/utils/references-falsy-initial-state.ts
29813
+ const isFalsyLiteral = (node) => {
29814
+ if (!node) return true;
29815
+ if (isNodeOfType(node, "Literal")) return !node.value;
29816
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
29817
+ };
29818
+ const isFalsyInitialStateBinding = (identifier) => {
29819
+ if (!isNodeOfType(identifier, "Identifier")) return false;
29820
+ const binding = findVariableInitializer(identifier, identifier.name);
29821
+ if (!binding) return false;
29822
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29823
+ if (!declarator?.init) return false;
29824
+ const init = stripParenExpression(declarator.init);
29825
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
29826
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
29827
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
29828
+ return isFalsyLiteral(init.arguments?.[0]);
29829
+ };
29830
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
29831
+ //#endregion
29832
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29833
+ const isGatedByFalsyInitialState = (node) => {
29834
+ let cursor = node;
29835
+ let parent = node.parent;
29836
+ while (parent) {
29837
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
29838
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29839
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29840
+ cursor = parent;
29841
+ parent = parent.parent ?? null;
29842
+ }
29843
+ return false;
29844
+ };
29845
+ //#endregion
29846
+ //#region src/plugin/utils/references-client-only-flag.ts
29847
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
29848
+ const referencesClientOnlyFlag = (expression) => {
29849
+ const unwrapped = stripParenExpression(expression);
29850
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
29851
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
29852
+ const property = unwrapped.property;
29853
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
29854
+ }
29855
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
29856
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
29857
+ return false;
29858
+ };
29859
+ //#endregion
29860
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
29861
+ const isInsideClientOnlyGuard = (node) => {
29862
+ let cursor = node;
29863
+ let parent = node.parent;
29864
+ while (parent) {
29865
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
29866
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
29867
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
29868
+ cursor = parent;
29869
+ parent = parent.parent ?? null;
29870
+ }
29871
+ return false;
29872
+ };
29873
+ //#endregion
29874
+ //#region src/plugin/rules/performance/no-locale-format-in-render.ts
29875
+ const LOCALE_FORMAT_METHOD_NAMES = new Set([
29876
+ "toLocaleString",
29877
+ "toLocaleDateString",
29878
+ "toLocaleTimeString"
29879
+ ]);
29880
+ const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
29881
+ const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
29882
+ const INTL_FORMAT_METHOD_NAMES = new Set([
29883
+ "format",
29884
+ "formatToParts",
29885
+ "formatRange"
29886
+ ]);
29887
+ const isProvableDateExpression = (expression) => {
29888
+ if (!expression) return false;
29889
+ const unwrapped = stripParenExpression(expression);
29890
+ return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
29891
+ };
29892
+ const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
29893
+ const receiverNameLooksDateFlavored = (expression) => {
29894
+ if (!expression) return false;
29895
+ const unwrapped = stripParenExpression(expression);
29896
+ if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
29897
+ if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
29898
+ if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
29899
+ return false;
29900
+ };
29901
+ const objectLiteralHasProperty = (objectExpression, propertyName) => {
29902
+ if (!objectExpression) return false;
29903
+ const unwrapped = stripParenExpression(objectExpression);
29904
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
29905
+ for (const property of unwrapped.properties ?? []) {
29906
+ if (!isNodeOfType(property, "Property")) continue;
29907
+ if (property.computed) continue;
29908
+ if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
29909
+ if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
29910
+ }
29911
+ return false;
29912
+ };
29913
+ const hasExplicitLocaleArgument = (argument) => {
29914
+ if (!argument) return false;
29915
+ const unwrapped = stripParenExpression(argument);
29916
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
29917
+ return true;
29918
+ };
29919
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
29920
+ const localeArgument = call.arguments?.[0];
29921
+ if (!hasExplicitLocaleArgument(localeArgument)) return false;
29922
+ const optionsArgument = call.arguments?.[1];
29923
+ if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
29924
+ return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
29925
+ };
29926
+ const matchLocaleMethodCall = (call) => {
29927
+ const callee = call.callee;
29928
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29929
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29930
+ const methodName = callee.property.name;
29931
+ if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
29932
+ const receiverIsProvablyDate = isProvableDateExpression(callee.object);
29933
+ if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
29934
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
29935
+ return {
29936
+ node: call,
29937
+ display: `${methodName}()`
29938
+ };
29939
+ };
29940
+ const getIntlFormatterName = (expression) => {
29941
+ if (!expression) return null;
29942
+ const unwrapped = stripParenExpression(expression);
29943
+ if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
29944
+ const callee = unwrapped.callee;
29945
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29946
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
29947
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29948
+ return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
29949
+ };
29950
+ const isDeterministicIntlConstruction = (construction, formatterName) => {
29951
+ if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
29952
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
29953
+ if (formatterName !== "DateTimeFormat") return true;
29954
+ return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
29955
+ };
29956
+ const matchIntlFormatCall = (call) => {
29957
+ const callee = call.callee;
29958
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29959
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29960
+ if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
29961
+ let construction = stripParenExpression(callee.object);
29962
+ if (isNodeOfType(construction, "Identifier")) {
29963
+ const binding = findVariableInitializer(construction, construction.name);
29964
+ construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
29965
+ }
29966
+ if (!construction) return null;
29967
+ const formatterName = getIntlFormatterName(construction);
29968
+ if (!formatterName) return null;
29969
+ if (isDeterministicIntlConstruction(construction, formatterName)) return null;
29970
+ return {
29971
+ node: call,
29972
+ display: `Intl.${formatterName}().${callee.property.name}()`
29973
+ };
29974
+ };
29975
+ const isDeterministicInputDateConstruction = (expression) => {
29976
+ if (!expression) return false;
29977
+ const unwrapped = stripParenExpression(expression);
29978
+ if (!isNodeOfType(unwrapped, "NewExpression")) return false;
29979
+ if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
29980
+ return (unwrapped.arguments?.length ?? 0) > 0;
29981
+ };
29982
+ const matchDateDefaultStringification = (node) => {
29983
+ if (isNodeOfType(node, "CallExpression")) {
29984
+ const callee = node.callee;
29985
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
29986
+ node,
29987
+ display: "Date.prototype.toString()"
29988
+ };
29989
+ if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
29990
+ node,
29991
+ display: "String(new Date(…))"
29992
+ };
29993
+ return null;
29994
+ }
29995
+ if (isNodeOfType(node, "TemplateLiteral")) {
29996
+ for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
29997
+ node: expression,
29998
+ display: "`${new Date(…)}`"
29999
+ };
30000
+ }
30001
+ return null;
30002
+ };
30003
+ const findRenderPhaseComponentOrHook = (node) => {
30004
+ let functionNode = findEnclosingFunction(node);
30005
+ while (functionNode) {
30006
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
30007
+ if (!executesDuringRender(functionNode)) return null;
30008
+ functionNode = findEnclosingFunction(functionNode);
30009
+ }
30010
+ return null;
30011
+ };
30012
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30013
+ if (fileHasUseClientDirective) return true;
30014
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30015
+ if (displayName && isReactHookName(displayName)) return true;
30016
+ let callsHook = false;
30017
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30018
+ if (callsHook) return false;
30019
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30020
+ callsHook = true;
30021
+ return false;
30022
+ }
30023
+ });
30024
+ return callsHook;
30025
+ };
30026
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
30027
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30028
+ if (!isNodeOfType(body, "BlockStatement")) return false;
30029
+ const ancestors = /* @__PURE__ */ new Set();
30030
+ let cursor = node;
30031
+ while (cursor) {
30032
+ ancestors.add(cursor);
30033
+ cursor = cursor.parent ?? null;
30034
+ }
30035
+ for (const statement of body.body ?? []) {
30036
+ if (ancestors.has(statement)) return false;
30037
+ if (!isNodeOfType(statement, "IfStatement")) continue;
30038
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
30039
+ let returnsEarly = false;
30040
+ walkAst(statement.consequent, (child) => {
30041
+ if (isFunctionLike$1(child)) return false;
30042
+ if (isNodeOfType(child, "ReturnStatement")) {
30043
+ returnsEarly = true;
30044
+ return false;
30045
+ }
30046
+ });
30047
+ if (returnsEarly) return true;
30048
+ }
30049
+ return false;
30050
+ };
30051
+ const findEnclosingJsxOpeningElement = (node) => {
30052
+ let cursor = node.parent;
30053
+ while (cursor) {
30054
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30055
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
30056
+ cursor = cursor.parent ?? null;
30057
+ }
30058
+ return null;
30059
+ };
30060
+ const noLocaleFormatInRender = defineRule({
30061
+ id: "no-locale-format-in-render",
30062
+ title: "Locale/timezone formatting during render",
30063
+ severity: "warn",
30064
+ category: "Correctness",
30065
+ disabledWhen: [
30066
+ "vite",
30067
+ "cra",
30068
+ "expo",
30069
+ "react-native",
30070
+ "unknown"
30071
+ ],
30072
+ recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
30073
+ create: (context) => {
30074
+ if (isTestlikeFilename(context.filename)) return {};
30075
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
30076
+ let fileHasUseClientDirective = false;
30077
+ let fileIsEmailTemplate = false;
30078
+ const reportedNodes = /* @__PURE__ */ new Set();
30079
+ const reportIfRenderPhase = (match) => {
30080
+ if (reportedNodes.has(match.node)) return;
30081
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30082
+ if (!componentOrHookNode) return;
30083
+ if (fileIsEmailTemplate) return;
30084
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30085
+ if (isInsideClientOnlyGuard(match.node)) return;
30086
+ if (isGatedByFalsyInitialState(match.node)) return;
30087
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30088
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30089
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30090
+ reportedNodes.add(match.node);
30091
+ context.report({
30092
+ node: match.node,
30093
+ message: `This can cause a hydration mismatch because ${match.display} formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
30094
+ });
30095
+ };
30096
+ return {
30097
+ Program(node) {
30098
+ fileHasUseClientDirective = hasDirective(node, "use client");
30099
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30100
+ },
30101
+ CallExpression(node) {
30102
+ const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30103
+ if (match) reportIfRenderPhase(match);
30104
+ },
30105
+ TemplateLiteral(node) {
30106
+ const match = matchDateDefaultStringification(node);
30107
+ if (match) reportIfRenderPhase(match);
30108
+ },
30109
+ JSXExpressionContainer(node) {
30110
+ const expression = stripParenExpression(node.expression);
30111
+ if (!isNodeOfType(expression, "CallExpression")) return;
30112
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
30113
+ const helperName = expression.callee.name;
30114
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30115
+ if (!componentOrHookNode) return;
30116
+ const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30117
+ if (!helperNode || !isFunctionLike$1(helperNode)) return;
30118
+ if (componentOrHookDisplayNameForFunction(helperNode)) return;
30119
+ walkAst(helperNode.body ?? helperNode, (child) => {
30120
+ if (isFunctionLike$1(child)) return false;
30121
+ if (!isNodeOfType(child, "CallExpression")) return;
30122
+ const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30123
+ if (!match || reportedNodes.has(match.node)) return;
30124
+ if (fileIsEmailTemplate) return;
30125
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30126
+ if (isInsideClientOnlyGuard(node)) return;
30127
+ if (isGatedByFalsyInitialState(node)) return;
30128
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30129
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30130
+ reportedNodes.add(match.node);
30131
+ context.report({
30132
+ node: match.node,
30133
+ message: `This can cause a hydration mismatch because ${match.display} (reached from JSX through "${helperName}") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
30134
+ });
30135
+ });
30136
+ }
30137
+ };
30138
+ }
30139
+ });
30140
+ //#endregion
29302
30141
  //#region src/plugin/rules/design/no-long-transition-duration.ts
29303
30142
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
29304
30143
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -30112,7 +30951,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30112
30951
  "reverse",
30113
30952
  "sort"
30114
30953
  ]);
30115
- const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30116
30954
  const OBJECT_MUTATION_METHODS = new Set([
30117
30955
  "assign",
30118
30956
  "defineProperties",
@@ -30186,7 +31024,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30186
31024
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30187
31025
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30188
31026
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30189
- if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30190
31027
  }
30191
31028
  }
30192
31029
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30225,6 +31062,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30225
31062
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30226
31063
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30227
31064
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
31065
+ if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
30228
31066
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30229
31067
  });
30230
31068
  return mutations;
@@ -30457,7 +31295,7 @@ const noNestedComponentDefinition = defineRule({
30457
31295
  id: "no-nested-component-definition",
30458
31296
  title: "Component defined inside another component",
30459
31297
  tags: ["test-noise", "react-jsx-only"],
30460
- severity: "error",
31298
+ severity: "warn",
30461
31299
  category: "Correctness",
30462
31300
  recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
30463
31301
  create: (context) => {
@@ -31412,12 +32250,12 @@ const noPassDataToParent = defineRule({
31412
32250
  if (calleeNode === identifier) {
31413
32251
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
31414
32252
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
31415
- } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32253
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31416
32254
  if (!isWholePropsObjectReference(analysis, ref)) continue;
31417
32255
  if (isCustomHookParameter(ref)) continue;
31418
32256
  } else continue;
31419
32257
  const methodName = getCallMethodName(calleeNode);
31420
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32258
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31421
32259
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31422
32260
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
31423
32261
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -31608,7 +32446,7 @@ const noPassLiveStateToParent = defineRule({
31608
32446
  if (isCallResultConsumedAsArgument(callExpr)) continue;
31609
32447
  const calleeNode = callExpr.callee;
31610
32448
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
31611
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32449
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31612
32450
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31613
32451
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
31614
32452
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -31936,40 +32774,6 @@ const noPreventDefault = defineRule({
31936
32774
  }
31937
32775
  });
31938
32776
  //#endregion
31939
- //#region src/plugin/utils/is-result-discarded-call.ts
31940
- const isResultDiscardedCall = (callExpression) => {
31941
- let node = callExpression;
31942
- let parent = node.parent;
31943
- while (parent) {
31944
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
31945
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
31946
- if (isNodeOfType(parent, "ChainExpression")) {
31947
- node = parent;
31948
- parent = node.parent;
31949
- continue;
31950
- }
31951
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
31952
- node = parent;
31953
- parent = node.parent;
31954
- continue;
31955
- }
31956
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
31957
- node = parent;
31958
- parent = node.parent;
31959
- continue;
31960
- }
31961
- if (isNodeOfType(parent, "SequenceExpression")) {
31962
- const expressions = parent.expressions ?? [];
31963
- if (expressions[expressions.length - 1] !== node) return true;
31964
- node = parent;
31965
- parent = node.parent;
31966
- continue;
31967
- }
31968
- return false;
31969
- }
31970
- return false;
31971
- };
31972
- //#endregion
31973
32777
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
31974
32778
  const isRefLatchGuardedEffect = (callbackBody) => {
31975
32779
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -32176,7 +32980,7 @@ const isAlwaysFreshExpression = (expression) => {
32176
32980
  return `${callee.name}()`;
32177
32981
  }
32178
32982
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32179
- const receiver = callee.object;
32983
+ const receiver = stripParenExpression(callee.object);
32180
32984
  const property = callee.property;
32181
32985
  if (!isNodeOfType(property, "Identifier")) return null;
32182
32986
  if (isNodeOfType(receiver, "Identifier")) {
@@ -32297,8 +33101,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32297
33101
  if (typeof sourceValue !== "string") return;
32298
33102
  if (handleExtraSource?.(node, context)) return;
32299
33103
  if (sourceValue !== source) return;
33104
+ if (isTypeOnlyImport(node)) return;
32300
33105
  for (const specifier of node.specifiers ?? []) {
32301
33106
  if (isNodeOfType(specifier, "ImportSpecifier")) {
33107
+ if (specifier.importKind === "type") continue;
32302
33108
  const importedName = getImportedName$1(specifier);
32303
33109
  if (!importedName) continue;
32304
33110
  const message = messages.get(importedName);
@@ -32317,8 +33123,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32317
33123
  MemberExpression(node) {
32318
33124
  if (namespaceBindings.size === 0) return;
32319
33125
  if (node.computed) return;
32320
- if (!isNodeOfType(node.object, "Identifier")) return;
32321
- if (!namespaceBindings.has(node.object.name)) return;
33126
+ const receiver = stripParenExpression(node.object);
33127
+ if (!isNodeOfType(receiver, "Identifier")) return;
33128
+ if (!namespaceBindings.has(receiver.name)) return;
32322
33129
  if (!isNodeOfType(node.property, "Identifier")) return;
32323
33130
  const message = messages.get(node.property.name);
32324
33131
  if (message) context.report({
@@ -32383,7 +33190,7 @@ const noReactDomDeprecatedApis = defineRule({
32383
33190
  });
32384
33191
  //#endregion
32385
33192
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
32386
- const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
33193
+ const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
32387
33194
  const isVendoredShadcnUiFilename = (rawFilename) => {
32388
33195
  if (!rawFilename) return false;
32389
33196
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32421,7 +33228,7 @@ const noReact19DeprecatedApis = defineRule({
32421
33228
  requires: ["react:19"],
32422
33229
  tags: ["test-noise", "migration-hint"],
32423
33230
  severity: "warn",
32424
- recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
33231
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32425
33232
  create: (context) => {
32426
33233
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32427
33234
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -32745,34 +33552,11 @@ const noRedundantShouldComponentUpdate = defineRule({
32745
33552
  });
32746
33553
  //#endregion
32747
33554
  //#region src/plugin/rules/architecture/no-render-in-render.ts
32748
- const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32749
- if (!symbol || visitedSymbols.has(symbol)) return false;
32750
- visitedSymbols.add(symbol);
32751
- if (isComponentParameterSymbol(symbol)) return true;
32752
- if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
32753
- const source = symbol.initializer;
32754
- if (!source) return false;
32755
- return initializerRootsInProps(source, scopes, visitedSymbols);
32756
- };
32757
- const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32758
- if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
32759
- if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
32760
- return rootsInProps(node, scopes, visitedSymbols);
32761
- };
32762
- const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32763
- let current = node;
32764
- while (isNodeOfType(current, "MemberExpression")) {
32765
- if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
32766
- current = current.object;
32767
- }
32768
- if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
32769
- return false;
32770
- };
32771
33555
  const isInsideComponentContext = (node) => {
32772
33556
  let cursor = node.parent;
32773
33557
  while (cursor) {
32774
- if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
32775
33558
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33559
+ if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
32776
33560
  cursor = cursor.parent ?? null;
32777
33561
  }
32778
33562
  return false;
@@ -32782,24 +33566,28 @@ const functionBodyOf = (node) => {
32782
33566
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
32783
33567
  return null;
32784
33568
  };
33569
+ const isHookCallee = (callee) => {
33570
+ if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
33571
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
33572
+ return false;
33573
+ };
32785
33574
  const containsHookCall = (body) => {
32786
33575
  let found = false;
32787
33576
  walkAst(body, (child) => {
32788
- if (found) return;
33577
+ if (found) return false;
33578
+ if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32789
33579
  if (!isNodeOfType(child, "CallExpression")) return;
32790
- const name = getCalleeName$2(child);
32791
- if (name && isReactHookName(name)) found = true;
33580
+ if (isHookCallee(child.callee)) found = true;
32792
33581
  });
32793
33582
  return found;
32794
33583
  };
32795
- const isModuleScopeHookFreeHelper = (symbol) => {
33584
+ const isHookCallingRenderHelper = (symbol) => {
32796
33585
  if (!symbol) return false;
32797
33586
  const declaration = symbol.declarationNode;
32798
33587
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
32799
33588
  const body = functionBodyOf(declaration);
32800
33589
  if (!body) return false;
32801
- if (findEnclosingFunction(declaration) !== null) return false;
32802
- return !containsHookCall(body);
33590
+ return containsHookCall(body);
32803
33591
  };
32804
33592
  const noRenderInRender = defineRule({
32805
33593
  id: "no-render-in-render",
@@ -32808,20 +33596,13 @@ const noRenderInRender = defineRule({
32808
33596
  tags: ["test-noise"],
32809
33597
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
32810
33598
  create: (context) => ({ JSXExpressionContainer(node) {
32811
- const expression = node.expression;
33599
+ const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
32812
33600
  if (!isNodeOfType(expression, "CallExpression")) return;
32813
- let calleeName = null;
32814
- if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
32815
- else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
32816
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33601
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
33602
+ const calleeName = expression.callee.name;
33603
+ if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32817
33604
  if (!isInsideComponentContext(node)) return;
32818
- if (isNodeOfType(expression.callee, "Identifier")) {
32819
- const calleeSymbol = context.scopes.symbolFor(expression.callee);
32820
- if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
32821
- if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
32822
- } else if (isNodeOfType(expression.callee, "MemberExpression")) {
32823
- if (rootsInProps(expression.callee.object, context.scopes)) return;
32824
- }
33605
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32825
33606
  context.report({
32826
33607
  node: expression,
32827
33608
  message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
@@ -32882,13 +33663,7 @@ const noRenderPropChildren = defineRule({
32882
33663
  //#endregion
32883
33664
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
32884
33665
  const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
32885
- const isReactDomRenderCall = (node) => {
32886
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
32887
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
32888
- if (node.callee.object.name !== "ReactDOM") return false;
32889
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
32890
- return node.callee.property.name === "render";
32891
- };
33666
+ const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
32892
33667
  const isUsedAsReturnValue = (parent) => {
32893
33668
  if (!parent) return false;
32894
33669
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -33724,7 +34499,7 @@ const noSetState = defineRule({
33724
34499
  category: "Architecture",
33725
34500
  create: (context) => ({ CallExpression(node) {
33726
34501
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
33727
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
34502
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33728
34503
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
33729
34504
  if (!getParentComponent(node)) return;
33730
34505
  context.report({
@@ -33890,6 +34665,204 @@ const noSideTabBorder = defineRule({
33890
34665
  })
33891
34666
  });
33892
34667
  //#endregion
34668
+ //#region src/plugin/rules/state-and-effects/no-stale-timer-ref.ts
34669
+ const GLOBAL_TIMER_RECEIVER_NAMES = new Set([
34670
+ "window",
34671
+ "globalThis",
34672
+ "self"
34673
+ ]);
34674
+ const NULLISH_COMPARISON_OPERATORS = new Set([
34675
+ "==",
34676
+ "===",
34677
+ "!=",
34678
+ "!=="
34679
+ ]);
34680
+ const getGlobalTimerCalleeName = (node) => {
34681
+ if (!isNodeOfType(node, "CallExpression")) return null;
34682
+ const callee = stripParenExpression(node.callee);
34683
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
34684
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
34685
+ const receiver = stripParenExpression(callee.object);
34686
+ if (isNodeOfType(receiver, "Identifier") && GLOBAL_TIMER_RECEIVER_NAMES.has(receiver.name)) return callee.property.name;
34687
+ }
34688
+ return null;
34689
+ };
34690
+ const getRefCurrentReceiverName = (node) => {
34691
+ if (!isNodeOfType(node, "MemberExpression") || node.computed) return null;
34692
+ if (!isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
34693
+ const receiver = stripParenExpression(node.object);
34694
+ return isNodeOfType(receiver, "Identifier") ? receiver.name : null;
34695
+ };
34696
+ const isRefCurrentMemberOf = (node, refName) => getRefCurrentReceiverName(node) === refName;
34697
+ const parseClearCallOnTimerRef = (node) => {
34698
+ const clearCalleeName = getGlobalTimerCalleeName(node);
34699
+ if (clearCalleeName === null || !TIMER_CLEANUP_CALLEE_NAMES.has(clearCalleeName)) return null;
34700
+ if (!isNodeOfType(node, "CallExpression")) return null;
34701
+ const clearedArgument = node.arguments?.[0];
34702
+ if (!clearedArgument) return null;
34703
+ const refName = getRefCurrentReceiverName(stripParenExpression(clearedArgument));
34704
+ return refName === null ? null : {
34705
+ clearCalleeName,
34706
+ refName
34707
+ };
34708
+ };
34709
+ const isClearCallOnRef = (node, refName) => parseClearCallOnTimerRef(node)?.refName === refName;
34710
+ const isNullishResetExpression = (node) => isNullishExpression(stripParenExpression(node));
34711
+ const isAssignmentToRefCurrent = (node, refName) => isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isRefCurrentMemberOf(node.left, refName);
34712
+ const isClearGuardIfIdiom = (ifStatement, refName) => {
34713
+ if (ifStatement.alternate) return false;
34714
+ const consequent = ifStatement.consequent;
34715
+ return (isNodeOfType(consequent, "BlockStatement") ? consequent.body ?? [] : [consequent]).every((statement) => {
34716
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
34717
+ const expression = stripParenExpression(statement.expression);
34718
+ if (isClearCallOnRef(expression, refName)) return true;
34719
+ return isAssignmentToRefCurrent(expression, refName) && isNullishResetExpression(expression.right);
34720
+ });
34721
+ };
34722
+ const climbBooleanProjection = (refCurrentMember) => {
34723
+ let cursor = refCurrentMember;
34724
+ const logicalAncestors = [];
34725
+ let didPassBooleanProjection = false;
34726
+ while (cursor.parent) {
34727
+ const parent = cursor.parent;
34728
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type)) {
34729
+ cursor = parent;
34730
+ continue;
34731
+ }
34732
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
34733
+ didPassBooleanProjection = true;
34734
+ cursor = parent;
34735
+ continue;
34736
+ }
34737
+ if (isNodeOfType(parent, "BinaryExpression") && NULLISH_COMPARISON_OPERATORS.has(parent.operator) && isNullishResetExpression(parent.left === cursor ? parent.right : parent.left)) {
34738
+ didPassBooleanProjection = true;
34739
+ cursor = parent;
34740
+ continue;
34741
+ }
34742
+ if (isNodeOfType(parent, "LogicalExpression")) {
34743
+ logicalAncestors.push(parent);
34744
+ cursor = parent;
34745
+ continue;
34746
+ }
34747
+ break;
34748
+ }
34749
+ return {
34750
+ conditionRoot: cursor,
34751
+ logicalAncestors,
34752
+ didPassBooleanProjection
34753
+ };
34754
+ };
34755
+ const isLogicalClearGuardIdiom = (logicalAncestors, refName) => logicalAncestors.some((logical) => logical.operator === "&&" && isClearCallOnRef(stripParenExpression(logical.right), refName));
34756
+ const isPendingSignalRead = (refCurrentMember, refName) => {
34757
+ const { conditionRoot, logicalAncestors, didPassBooleanProjection } = climbBooleanProjection(refCurrentMember);
34758
+ const conditionParent = conditionRoot.parent;
34759
+ if (isNodeOfType(conditionParent, "IfStatement") && conditionParent.test === conditionRoot) return !isClearGuardIfIdiom(conditionParent, refName);
34760
+ if ((isNodeOfType(conditionParent, "ConditionalExpression") || isNodeOfType(conditionParent, "WhileStatement") || isNodeOfType(conditionParent, "DoWhileStatement") || isNodeOfType(conditionParent, "ForStatement")) && conditionParent.test === conditionRoot) return true;
34761
+ if (logicalAncestors.length > 0) return !isLogicalClearGuardIdiom(logicalAncestors, refName);
34762
+ return didPassBooleanProjection;
34763
+ };
34764
+ const collectTimerRefUsageFacts = (ownerScope, refName) => {
34765
+ const facts = {
34766
+ holdsScheduledTimerId: false,
34767
+ hasPendingSignalRead: false
34768
+ };
34769
+ walkAst(ownerScope, (child) => {
34770
+ if (isAssignmentToRefCurrent(child, refName)) {
34771
+ const assignedCalleeName = getGlobalTimerCalleeName(stripParenExpression(child.right));
34772
+ if (assignedCalleeName !== null && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(assignedCalleeName)) facts.holdsScheduledTimerId = true;
34773
+ return;
34774
+ }
34775
+ if (isRefCurrentMemberOf(child, refName) && isPendingSignalRead(child, refName)) facts.hasPendingSignalRead = true;
34776
+ });
34777
+ return facts;
34778
+ };
34779
+ const isEffectCallbackFunction = (functionNode) => {
34780
+ const parent = functionNode.parent;
34781
+ if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
34782
+ return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
34783
+ };
34784
+ const doesEffectCallbackReturnName = (effectCallback, name) => {
34785
+ if (!isFunctionLike$1(effectCallback)) return false;
34786
+ let isReturnedByName = false;
34787
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
34788
+ if (isReturnedByName || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
34789
+ const returned = stripParenExpression(child.argument);
34790
+ if (isNodeOfType(returned, "Identifier") && returned.name === name) isReturnedByName = true;
34791
+ });
34792
+ return isReturnedByName;
34793
+ };
34794
+ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
34795
+ if (!isFunctionLike$1(effectCallback)) return false;
34796
+ if (isNodeOfType(functionNode.parent, "ReturnStatement")) return true;
34797
+ if (effectCallback.body === functionNode) return true;
34798
+ const cleanupBindingName = getFunctionBindingName$1(functionNode);
34799
+ return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
34800
+ };
34801
+ const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
34802
+ const cleanupBindingName = getFunctionBindingName$1(functionNode);
34803
+ if (cleanupBindingName === null) return false;
34804
+ let isReturnedFromEffect = false;
34805
+ walkAst(ownerScope, (child) => {
34806
+ if (isReturnedFromEffect) return false;
34807
+ if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
34808
+ const effectCallback = getEffectCallback(child);
34809
+ if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
34810
+ });
34811
+ return isReturnedFromEffect;
34812
+ };
34813
+ const isInsideEffectCleanupReturn = (node, ownerScope) => {
34814
+ let functionNode = findEnclosingFunction(node);
34815
+ while (functionNode) {
34816
+ const outerFunction = findEnclosingFunction(functionNode);
34817
+ if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
34818
+ if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
34819
+ functionNode = outerFunction;
34820
+ }
34821
+ return false;
34822
+ };
34823
+ const hasRefCurrentReassignmentAfterClear = (clearCall, refName) => {
34824
+ const enclosingFunction = findEnclosingFunction(clearCall);
34825
+ if (!isFunctionLike$1(enclosingFunction)) return false;
34826
+ if (!isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
34827
+ const clearStart = getRangeStart(clearCall);
34828
+ if (clearStart === null) return false;
34829
+ let didFindLaterReassignment = false;
34830
+ walkInsideStatementBlocks(enclosingFunction.body, (child) => {
34831
+ if (didFindLaterReassignment) return;
34832
+ if (!isAssignmentToRefCurrent(child, refName)) return;
34833
+ const assignmentStart = getRangeStart(child);
34834
+ if (assignmentStart !== null && assignmentStart > clearStart) didFindLaterReassignment = true;
34835
+ });
34836
+ return didFindLaterReassignment;
34837
+ };
34838
+ const isShadowedTimerGlobal = (clearCall) => {
34839
+ const callee = stripParenExpression(clearCall.callee);
34840
+ if (!isNodeOfType(callee, "Identifier")) return false;
34841
+ return findVariableInitializer(callee, callee.name) !== null;
34842
+ };
34843
+ const noStaleTimerRef = defineRule({
34844
+ id: "no-stale-timer-ref",
34845
+ title: "Cleared timer ref keeps the stale id",
34846
+ severity: "warn",
34847
+ recommendation: "Reset the ref right after clearing (`clearTimeout(ref.current); ref.current = null`) so truthiness checks on the ref keep meaning “timer still pending”.",
34848
+ create: (context) => ({ CallExpression(node) {
34849
+ const clearCall = parseClearCallOnTimerRef(node);
34850
+ if (!clearCall) return;
34851
+ if (isShadowedTimerGlobal(node)) return;
34852
+ const { clearCalleeName, refName } = clearCall;
34853
+ const refBinding = findVariableInitializer(node, refName);
34854
+ if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
34855
+ const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
34856
+ if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
34857
+ if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
34858
+ if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
34859
+ context.report({
34860
+ node,
34861
+ message: `\`${clearCalleeName}(${refName}.current)\` cancels the timer but leaves the old id in \`${refName}.current\`, and this component reads \`${refName}.current\` as a \u201Ctimer pending\u201D signal — assign \`${refName}.current = null\` right after clearing so a cancelled timer does not look pending.`
34862
+ });
34863
+ } })
34864
+ });
34865
+ //#endregion
33893
34866
  //#region src/plugin/utils/is-abstract-role.ts
33894
34867
  const isAbstractRole = (openingElement, settings) => {
33895
34868
  const elementType = getElementType(openingElement, settings);
@@ -36170,6 +37143,36 @@ const isTriviallyCheapExpression = (node) => {
36170
37143
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36171
37144
  return true;
36172
37145
  };
37146
+ const isTrivialContainerLiteral = (node) => {
37147
+ if (!node) return false;
37148
+ const innerExpression = stripParenExpression(node);
37149
+ if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
37150
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
37151
+ return false;
37152
+ };
37153
+ const isNonEscapingRead = (identifier) => {
37154
+ const readRoot = findTransparentExpressionRoot(identifier);
37155
+ const memberNode = readRoot.parent;
37156
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
37157
+ const memberUse = findTransparentExpressionRoot(memberNode);
37158
+ const memberUseParent = memberUse.parent;
37159
+ if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
37160
+ if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
37161
+ if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
37162
+ return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
37163
+ };
37164
+ const isMemoIdentityUnused = (memoCallNode, scopes) => {
37165
+ const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
37166
+ const parentNode = memoUsageRoot.parent;
37167
+ if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
37168
+ if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
37169
+ const bindingTarget = parentNode.id;
37170
+ if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
37171
+ if (!isNodeOfType(bindingTarget, "Identifier")) return false;
37172
+ const symbol = scopes.symbolFor(bindingTarget);
37173
+ if (!symbol) return false;
37174
+ return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
37175
+ };
36173
37176
  const noUsememoSimpleExpression = defineRule({
36174
37177
  id: "no-usememo-simple-expression",
36175
37178
  title: "useMemo on a cheap value",
@@ -36191,9 +37194,17 @@ const noUsememoSimpleExpression = defineRule({
36191
37194
  let returnExpression = null;
36192
37195
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36193
37196
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36194
- if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
37197
+ if (!returnExpression) return;
37198
+ if (isTriviallyCheapExpression(returnExpression)) {
37199
+ context.report({
37200
+ node,
37201
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37202
+ });
37203
+ return;
37204
+ }
37205
+ if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36195
37206
  node,
36196
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37207
+ message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36197
37208
  });
36198
37209
  } })
36199
37210
  });
@@ -36299,7 +37310,7 @@ const noWillUpdateSetState = defineRule({
36299
37310
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
36300
37311
  return { CallExpression(node) {
36301
37312
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
36302
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
37313
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36303
37314
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36304
37315
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36305
37316
  context.report({
@@ -36575,8 +37586,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
36575
37586
  const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
36576
37587
  const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
36577
37588
  const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
36578
- const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
36579
- const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
37589
+ const NAMESPACE_OBJECT_MESSAGE = "This export bundles components inside an object, so Fast Refresh can't track them and falls back to a full reload.";
36580
37590
  const DEFAULT_REACT_HOCS = [
36581
37591
  "memo",
36582
37592
  "forwardRef",
@@ -36617,6 +37627,10 @@ const isRouteFactoryCall = (expression) => {
36617
37627
  }
36618
37628
  return false;
36619
37629
  };
37630
+ const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
37631
+ const expression = skipTsExpression(argument);
37632
+ return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
37633
+ });
36620
37634
  const isReactHocName = (name, state) => state.customHocs.has(name);
36621
37635
  const isHocCallee = (callee, state) => {
36622
37636
  if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
@@ -36647,6 +37661,20 @@ const isReactComponentInitializer = (expression, state) => {
36647
37661
  if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
36648
37662
  return false;
36649
37663
  };
37664
+ const objectExpressionBundlesComponents = (objectExpression, state) => {
37665
+ for (const property of objectExpression.properties ?? []) {
37666
+ if (!isNodeOfType(property, "Property")) continue;
37667
+ const value = skipTsExpression(property.value);
37668
+ if (isNodeOfType(value, "Identifier")) {
37669
+ if (state.localComponentNames.has(value.name)) return true;
37670
+ continue;
37671
+ }
37672
+ if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
37673
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
37674
+ if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
37675
+ }
37676
+ return false;
37677
+ };
36650
37678
  const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36651
37679
  if (initializer) {
36652
37680
  const expression = skipTsExpression(initializer);
@@ -36677,6 +37705,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36677
37705
  reportNode
36678
37706
  };
36679
37707
  }
37708
+ if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
37709
+ kind: "namespace-object",
37710
+ reportNode
37711
+ };
36680
37712
  if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
36681
37713
  kind: "non-component",
36682
37714
  reportNode
@@ -36693,7 +37725,7 @@ const collectRelevantNodes = (programRoot) => {
36693
37725
  walkAst(programRoot, (child) => {
36694
37726
  const childType = child.type;
36695
37727
  if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
36696
- else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
37728
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
36697
37729
  });
36698
37730
  return {
36699
37731
  exportNodes,
@@ -36738,18 +37770,35 @@ const onlyExportComponents = defineRule({
36738
37770
  category: "Architecture",
36739
37771
  create: (context) => {
36740
37772
  const settings = resolveSettings$6(context.settings);
36741
- const state = {
36742
- customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36743
- allowExportNames: new Set(settings.allowExportNames),
36744
- allowConstantExport: settings.allowConstantExport
36745
- };
36746
37773
  return { Program(node) {
36747
37774
  if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
36748
37775
  const { exportNodes, componentCandidates } = collectRelevantNodes(node);
37776
+ const localComponentNames = /* @__PURE__ */ new Set();
37777
+ const state = {
37778
+ customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
37779
+ allowExportNames: new Set(settings.allowExportNames),
37780
+ allowConstantExport: settings.allowConstantExport,
37781
+ localComponentNames,
37782
+ scopes: context.scopes
37783
+ };
37784
+ for (const child of componentCandidates) {
37785
+ if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
37786
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
37787
+ }
37788
+ if (isNodeOfType(child, "ClassDeclaration") && child.id) {
37789
+ if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37790
+ }
37791
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
37792
+ const initializer = child.init;
37793
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
37794
+ const expression = initializer ? skipTsExpression(initializer) : null;
37795
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
37796
+ }
37797
+ }
37798
+ }
36749
37799
  const exports = [];
36750
37800
  let hasReactExport = false;
36751
37801
  let hasAnyExports = false;
36752
- const localComponents = [];
36753
37802
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
36754
37803
  for (const child of exportNodes) {
36755
37804
  if (isNodeOfType(child, "ExportAllDeclaration")) {
@@ -36814,13 +37863,24 @@ const onlyExportComponents = defineRule({
36814
37863
  return false;
36815
37864
  })();
36816
37865
  if (isHoc && firstArgIsValid) hasReactExport = true;
37866
+ else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
37867
+ kind: "non-component",
37868
+ reportNode: stripped
37869
+ });
36817
37870
  else context.report({
36818
37871
  node: stripped,
36819
37872
  message: ANONYMOUS_MESSAGE
36820
37873
  });
36821
37874
  continue;
36822
37875
  }
36823
- if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
37876
+ if (isNodeOfType(stripped, "ObjectExpression")) {
37877
+ context.report({
37878
+ node: stripped,
37879
+ message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
37880
+ });
37881
+ continue;
37882
+ }
37883
+ if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
36824
37884
  context.report({
36825
37885
  node: stripped,
36826
37886
  message: ANONYMOUS_MESSAGE
@@ -36873,32 +37933,23 @@ const onlyExportComponents = defineRule({
36873
37933
  let entry;
36874
37934
  if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
36875
37935
  else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
36876
- else entry = {
36877
- kind: "non-component",
36878
- reportNode
36879
- };
37936
+ else {
37937
+ entry = {
37938
+ kind: "non-component",
37939
+ reportNode
37940
+ };
37941
+ if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
37942
+ }
36880
37943
  if (isReExportFromSource && entry.kind !== "react-component") continue;
36881
37944
  exports.push(entry);
36882
37945
  }
36883
37946
  }
36884
37947
  }
36885
37948
  for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
36886
- const isInsideExport = (node) => {
36887
- let walker = node.parent;
36888
- while (walker) {
36889
- if (isNodeOfType(walker, "ExportNamedDeclaration") || isNodeOfType(walker, "ExportDefaultDeclaration") || isNodeOfType(walker, "ExportAllDeclaration")) return true;
36890
- walker = walker.parent ?? null;
36891
- }
36892
- return false;
36893
- };
36894
- for (const child of componentCandidates) {
36895
- if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36896
- if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36897
- }
36898
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36899
- if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36900
- }
36901
- }
37949
+ for (const entry of exports) if (entry.kind === "namespace-object") context.report({
37950
+ node: entry.reportNode,
37951
+ message: NAMESPACE_OBJECT_MESSAGE
37952
+ });
36902
37953
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
36903
37954
  if (entry.kind === "non-component") context.report({
36904
37955
  node: entry.reportNode,
@@ -36909,14 +37960,6 @@ const onlyExportComponents = defineRule({
36909
37960
  message: REACT_CONTEXT_MESSAGE
36910
37961
  });
36911
37962
  }
36912
- else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
36913
- node: local,
36914
- message: LOCAL_COMPONENT_MESSAGE
36915
- });
36916
- else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
36917
- node: local,
36918
- message: NO_EXPORT_MESSAGE
36919
- });
36920
37963
  } };
36921
37964
  }
36922
37965
  });
@@ -37714,8 +38757,8 @@ const preferHtmlDialog = defineRule({
37714
38757
  if (!HTML_TAGS.has(tagName)) return;
37715
38758
  const roleAttribute = findJsxAttribute(node.attributes, "role");
37716
38759
  if (roleAttribute) {
37717
- const roleValue = getJsxPropStringValue(roleAttribute);
37718
- if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
38760
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
38761
+ if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37719
38762
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
37720
38763
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
37721
38764
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -38419,11 +39462,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
38419
39462
  }
38420
39463
  return null;
38421
39464
  };
39465
+ const isTrivialLiteralExpression = (expression) => {
39466
+ if (isNodeOfType(expression, "Literal")) return true;
39467
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
39468
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
39469
+ if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
39470
+ return false;
39471
+ };
38422
39472
  const getSingleSetterCallFromHandler = (handler) => {
38423
39473
  const handlerStatements = getCallbackStatements(handler);
38424
39474
  if (handlerStatements.length !== 1) return null;
38425
39475
  const onlyStatement = handlerStatements[0];
38426
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
39476
+ let expression = onlyStatement;
39477
+ if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
39478
+ if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
39479
+ if (!expression) return null;
39480
+ expression = stripParenExpression(expression);
38427
39481
  if (!isNodeOfType(expression, "CallExpression")) return null;
38428
39482
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
38429
39483
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -38433,6 +39487,106 @@ const getSingleSetterCallFromHandler = (handler) => {
38433
39487
  setterArgument: expression.arguments[0]
38434
39488
  };
38435
39489
  };
39490
+ const isListenerCollectionInitializer = (init) => {
39491
+ if (!init) return false;
39492
+ if (isNodeOfType(init, "ArrayExpression")) return true;
39493
+ return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
39494
+ };
39495
+ const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
39496
+ if (!isFunctionLike$1(functionNode)) return false;
39497
+ const firstParam = functionNode.params?.[0];
39498
+ if (!isNodeOfType(firstParam, "Identifier")) return false;
39499
+ const listenerParamName = firstParam.name;
39500
+ let registersListener = false;
39501
+ walkAst(functionNode.body, (child) => {
39502
+ if (registersListener) return false;
39503
+ if (!isNodeOfType(child, "CallExpression")) return;
39504
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
39505
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
39506
+ if (!listenerCollectionNames.has(child.callee.object.name)) return;
39507
+ if (!isNodeOfType(child.callee.property, "Identifier")) return;
39508
+ if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
39509
+ const registeredArgument = child.arguments?.[0];
39510
+ if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
39511
+ });
39512
+ return registersListener;
39513
+ };
39514
+ const buildModuleScopeStoreIndex = (programRoot) => {
39515
+ const mutableBindingNames = /* @__PURE__ */ new Set();
39516
+ const listenerCollectionNames = /* @__PURE__ */ new Set();
39517
+ const moduleFunctionsByName = /* @__PURE__ */ new Map();
39518
+ if (!isNodeOfType(programRoot, "Program")) return {
39519
+ mutableBindingNames,
39520
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39521
+ };
39522
+ for (const statement of programRoot.body ?? []) {
39523
+ const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
39524
+ if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
39525
+ moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
39526
+ continue;
39527
+ }
39528
+ if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
39529
+ for (const declarator of unwrapped.declarations ?? []) {
39530
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
39531
+ const init = declarator.init ?? null;
39532
+ if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
39533
+ mutableBindingNames.add(declarator.id.name);
39534
+ continue;
39535
+ }
39536
+ if (isListenerCollectionInitializer(init)) {
39537
+ listenerCollectionNames.add(declarator.id.name);
39538
+ continue;
39539
+ }
39540
+ if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
39541
+ }
39542
+ }
39543
+ const subscribeFunctionNames = /* @__PURE__ */ new Set();
39544
+ for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
39545
+ return {
39546
+ mutableBindingNames,
39547
+ subscribeFunctionNames
39548
+ };
39549
+ };
39550
+ const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
39551
+ if (!isNodeOfType(useStateCall, "CallExpression")) return null;
39552
+ let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
39553
+ if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
39554
+ if (!isNodeOfType(initialArgument, "Identifier")) return null;
39555
+ if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
39556
+ const binding = findVariableInitializer(initialArgument, initialArgument.name);
39557
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
39558
+ return initialArgument.name;
39559
+ };
39560
+ const argumentForwardsSetter = (argument, setterName) => {
39561
+ if (!argument) return false;
39562
+ const unwrapped = stripParenExpression(argument);
39563
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
39564
+ if (!isFunctionLike$1(unwrapped)) return false;
39565
+ let callsSetter = false;
39566
+ walkAst(unwrapped.body, (child) => {
39567
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
39568
+ callsSetter = true;
39569
+ return false;
39570
+ }
39571
+ });
39572
+ return callsSetter;
39573
+ };
39574
+ const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
39575
+ let matchedCall = null;
39576
+ walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
39577
+ if (matchedCall) return false;
39578
+ if (!isNodeOfType(child, "CallExpression")) return;
39579
+ if (!isNodeOfType(child.callee, "Identifier")) return;
39580
+ if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
39581
+ const binding = findVariableInitializer(child.callee, child.callee.name);
39582
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
39583
+ for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
39584
+ matchedCall = child;
39585
+ return false;
39586
+ }
39587
+ });
39588
+ return matchedCall;
39589
+ };
38436
39590
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
38437
39591
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
38438
39592
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -38449,6 +39603,16 @@ const preferUseSyncExternalStore = defineRule({
38449
39603
  severity: "warn",
38450
39604
  recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
38451
39605
  create: (context) => {
39606
+ let cachedStoreIndex = null;
39607
+ const storeIndexFor = (node) => {
39608
+ if (cachedStoreIndex) return cachedStoreIndex;
39609
+ const programRoot = findProgramRoot(node);
39610
+ cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
39611
+ mutableBindingNames: /* @__PURE__ */ new Set(),
39612
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39613
+ };
39614
+ return cachedStoreIndex;
39615
+ };
38452
39616
  const checkComponent = (componentBody) => {
38453
39617
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38454
39618
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -38486,6 +39650,7 @@ const preferUseSyncExternalStore = defineRule({
38486
39650
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
38487
39651
  if (!useStateInitializer) continue;
38488
39652
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
39653
+ if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
38489
39654
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
38490
39655
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
38491
39656
  context.report({
@@ -38493,14 +39658,47 @@ const preferUseSyncExternalStore = defineRule({
38493
39658
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
38494
39659
  });
38495
39660
  }
39661
+ checkModuleStoreShape(componentBody, useStateBindings);
39662
+ };
39663
+ const checkModuleStoreShape = (componentBody, useStateBindings) => {
39664
+ const storeIndex = storeIndexFor(componentBody);
39665
+ if (storeIndex.mutableBindingNames.size === 0) return;
39666
+ if (storeIndex.subscribeFunctionNames.size === 0) return;
39667
+ const snapshotBindings = useStateBindings.map((binding) => ({
39668
+ binding,
39669
+ storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
39670
+ })).filter((candidate) => candidate.storeName !== null);
39671
+ if (snapshotBindings.length === 0) return;
39672
+ const reportedDeclarators = /* @__PURE__ */ new Set();
39673
+ for (const effectCall of findUseEffectsInComponent(componentBody)) {
39674
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
39675
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
39676
+ const depsNode = effectCall.arguments[1];
39677
+ if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
39678
+ if ((depsNode.elements?.length ?? 0) !== 0) continue;
39679
+ const callback = getEffectCallback(effectCall);
39680
+ if (!callback || !isFunctionLike$1(callback)) continue;
39681
+ for (const { binding, storeName } of snapshotBindings) {
39682
+ if (reportedDeclarators.has(binding.declarator)) continue;
39683
+ if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
39684
+ reportedDeclarators.add(binding.declarator);
39685
+ context.report({
39686
+ node: binding.declarator,
39687
+ message: `Your users can miss updates or see torn values because useState "${binding.valueName}" snapshots module store "${storeName}" at render but only subscribes later in a useEffect.`
39688
+ });
39689
+ }
39690
+ }
38496
39691
  };
38497
39692
  return {
38498
39693
  FunctionDeclaration(node) {
38499
- if (!node.id?.name || !isUppercaseName(node.id.name)) return;
39694
+ const functionName = node.id?.name;
39695
+ if (!functionName) return;
39696
+ if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38500
39697
  checkComponent(node.body);
38501
39698
  },
38502
39699
  VariableDeclarator(node) {
38503
- if (!isComponentAssignment(node)) return;
39700
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
39701
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
38504
39702
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
38505
39703
  checkComponent(node.init.body);
38506
39704
  }
@@ -39110,7 +40308,7 @@ const queryNoVoidQueryFn = defineRule({
39110
40308
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
39111
40309
  const body = queryFnValue.body;
39112
40310
  if (!isNodeOfType(body, "BlockStatement")) return;
39113
- if ((body.body ?? []).length === 0) context.report({
40311
+ if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39114
40312
  node: queryFnProperty,
39115
40313
  message: "This empty queryFn caches undefined, so the component never gets data."
39116
40314
  });
@@ -39617,17 +40815,6 @@ const renderingHoistJsx = defineRule({
39617
40815
  }
39618
40816
  });
39619
40817
  //#endregion
39620
- //#region src/plugin/utils/find-declarator-for-binding.ts
39621
- const findDeclaratorForBinding = (bindingIdentifier) => {
39622
- let ancestor = bindingIdentifier.parent;
39623
- while (ancestor) {
39624
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
39625
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
39626
- ancestor = ancestor.parent ?? null;
39627
- }
39628
- return null;
39629
- };
39630
- //#endregion
39631
40818
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
39632
40819
  const NONDETERMINISTIC_RENDER_PATTERNS = [
39633
40820
  {
@@ -39636,19 +40823,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
39636
40823
  },
39637
40824
  {
39638
40825
  display: "Date.now()",
39639
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Date" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40826
+ matches: (node) => isGlobalMethodCall(node, "Date", "now")
39640
40827
  },
39641
40828
  {
39642
40829
  display: "Math.random()",
39643
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Math" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "random"
40830
+ matches: (node) => isGlobalMethodCall(node, "Math", "random")
39644
40831
  },
39645
40832
  {
39646
40833
  display: "performance.now()",
39647
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "performance" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40834
+ matches: (node) => isGlobalMethodCall(node, "performance", "now")
39648
40835
  },
39649
40836
  {
39650
40837
  display: "crypto.randomUUID()",
39651
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "crypto" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "randomUUID"
40838
+ matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39652
40839
  }
39653
40840
  ];
39654
40841
  const findOpeningElementOfChild = (jsxNode) => {
@@ -39660,54 +40847,6 @@ const findOpeningElementOfChild = (jsxNode) => {
39660
40847
  }
39661
40848
  return null;
39662
40849
  };
39663
- const executesDuringRender = (functionNode) => {
39664
- const parent = functionNode.parent;
39665
- if (!isNodeOfType(parent, "CallExpression")) return false;
39666
- if (parent.callee === functionNode) return true;
39667
- return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
39668
- };
39669
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
39670
- const referencesClientOnlyFlag = (expression) => {
39671
- const unwrapped = stripParenExpression(expression);
39672
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
39673
- if (isNodeOfType(unwrapped, "MemberExpression")) {
39674
- const property = unwrapped.property;
39675
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
39676
- }
39677
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
39678
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
39679
- return false;
39680
- };
39681
- const isFalsyLiteral = (node) => {
39682
- if (!node) return true;
39683
- if (isNodeOfType(node, "Literal")) return !node.value;
39684
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
39685
- };
39686
- const isFalsyInitialStateBinding = (identifier) => {
39687
- if (!isNodeOfType(identifier, "Identifier")) return false;
39688
- const binding = findVariableInitializer(identifier, identifier.name);
39689
- if (!binding) return false;
39690
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
39691
- if (!declarator?.init) return false;
39692
- const init = stripParenExpression(declarator.init);
39693
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
39694
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
39695
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
39696
- return isFalsyLiteral(init.arguments?.[0]);
39697
- };
39698
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
39699
- const isGatedByFalsyInitialState = (node) => {
39700
- let cursor = node;
39701
- let parent = node.parent;
39702
- while (parent) {
39703
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
39704
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39705
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39706
- cursor = parent;
39707
- parent = parent.parent ?? null;
39708
- }
39709
- return false;
39710
- };
39711
40850
  const isYearOnlyDateRead = (dateNode) => {
39712
40851
  const member = dateNode.parent;
39713
40852
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -39731,23 +40870,6 @@ const isInsideMotionTransitionAttribute = (node) => {
39731
40870
  }
39732
40871
  return false;
39733
40872
  };
39734
- const isInsideClientOnlyGuard = (node) => {
39735
- let cursor = node;
39736
- let parent = node.parent;
39737
- while (parent) {
39738
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
39739
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
39740
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
39741
- cursor = parent;
39742
- parent = parent.parent ?? null;
39743
- }
39744
- return false;
39745
- };
39746
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
39747
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
39748
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
39749
- return false;
39750
- };
39751
40873
  const renderingHydrationMismatchTime = defineRule({
39752
40874
  id: "rendering-hydration-mismatch-time",
39753
40875
  title: "Time or random value in JSX",
@@ -39795,6 +40917,35 @@ const renderingHydrationMismatchTime = defineRule({
39795
40917
  }
39796
40918
  });
39797
40919
  //#endregion
40920
+ //#region src/plugin/utils/contains-locale-environment-read.ts
40921
+ const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
40922
+ "toLocaleString",
40923
+ "toLocaleDateString",
40924
+ "toLocaleTimeString",
40925
+ "getTimezoneOffset"
40926
+ ]);
40927
+ const containsLocaleEnvironmentRead = (expression) => {
40928
+ let readsLocaleEnvironment = false;
40929
+ walkAst(expression, (child) => {
40930
+ if (readsLocaleEnvironment) return false;
40931
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
40932
+ if (child.object.name === "Intl") {
40933
+ readsLocaleEnvironment = true;
40934
+ return false;
40935
+ }
40936
+ if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
40937
+ readsLocaleEnvironment = true;
40938
+ return false;
40939
+ }
40940
+ }
40941
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && LOCALE_ENVIRONMENT_METHOD_NAMES.has(child.callee.property.name)) {
40942
+ readsLocaleEnvironment = true;
40943
+ return false;
40944
+ }
40945
+ });
40946
+ return readsLocaleEnvironment;
40947
+ };
40948
+ //#endregion
39798
40949
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
39799
40950
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
39800
40951
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -39860,14 +41011,15 @@ const renderingHydrationNoFlicker = defineRule({
39860
41011
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
39861
41012
  const callback = getEffectCallback(node);
39862
41013
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
39863
- const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
39864
- if (!bodyStatements || bodyStatements.length !== 1) return;
41014
+ const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
41015
+ if (bodyStatements.length !== 1) return;
39865
41016
  const soleStatement = bodyStatements[0];
39866
41017
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
39867
41018
  const expression = soleStatement.expression;
39868
41019
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
39869
41020
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
39870
41021
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
41022
+ if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
39871
41023
  context.report({
39872
41024
  node,
39873
41025
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -40686,6 +41838,9 @@ const rerenderFunctionalSetstate = defineRule({
40686
41838
  } })
40687
41839
  });
40688
41840
  //#endregion
41841
+ //#region src/plugin/utils/is-trivial-built-in-construction.ts
41842
+ const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
41843
+ //#endregion
40689
41844
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
40690
41845
  const rerenderLazyRefInit = defineRule({
40691
41846
  id: "rerender-lazy-ref-init",
@@ -40696,7 +41851,7 @@ const rerenderLazyRefInit = defineRule({
40696
41851
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
40697
41852
  create: (context) => ({ CallExpression(node) {
40698
41853
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
40699
- const initializer = node.arguments[0];
41854
+ const initializer = stripParenExpression(node.arguments[0]);
40700
41855
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
40701
41856
  const isNewCall = isNodeOfType(initializer, "NewExpression");
40702
41857
  if (!isPlainCall && !isNewCall) return;
@@ -40704,6 +41859,7 @@ const rerenderLazyRefInit = defineRule({
40704
41859
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40705
41860
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40706
41861
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
41862
+ if (isTrivialBuiltInConstruction(initializer)) return;
40707
41863
  if (isPlainCall && isReactHookName(calleeName)) return;
40708
41864
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40709
41865
  context.report({
@@ -40736,26 +41892,14 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40736
41892
  "getUTCMilliseconds",
40737
41893
  "valueOf"
40738
41894
  ]);
40739
- const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
40740
- "Date",
40741
- "Map",
40742
- "Set",
40743
- "WeakMap",
40744
- "WeakSet",
40745
- "RegExp",
40746
- "Error",
40747
- "URL",
40748
- "URLSearchParams",
40749
- "AbortController"
40750
- ]);
40751
41895
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40752
41896
  const findEagerInitializerCall = (expression, depth = 0) => {
40753
41897
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
40754
- if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
40755
- if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
40756
- if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
40757
- if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
40758
- if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
41898
+ const innerExpression = stripParenExpression(expression);
41899
+ if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
41900
+ if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
41901
+ if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
41902
+ if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
40759
41903
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
40760
41904
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
40761
41905
  if (spreadCall) return spreadCall;
@@ -40778,7 +41922,7 @@ const rerenderLazyStateInit = defineRule({
40778
41922
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40779
41923
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40780
41924
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40781
- if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41925
+ if (isTrivialBuiltInConstruction(initializer)) return;
40782
41926
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
40783
41927
  if (isReactHookName(calleeName)) return;
40784
41928
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -41445,7 +42589,7 @@ const rnAnimationReactionAsDerived = defineRule({
41445
42589
  const body = reactionFn.body;
41446
42590
  let singleAssignment = null;
41447
42591
  if (isNodeOfType(body, "BlockStatement")) {
41448
- const statements = body.body ?? [];
42592
+ const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41449
42593
  if (statements.length !== 1) return;
41450
42594
  const onlyStatement = statements[0];
41451
42595
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -41491,7 +42635,7 @@ const rnBottomSheetPreferNative = defineRule({
41491
42635
  });
41492
42636
  //#endregion
41493
42637
  //#region src/plugin/rules/react-native/rn-detox-missing-await.ts
41494
- const EMPTY_VISITORS$4 = {};
42638
+ const EMPTY_VISITORS$5 = {};
41495
42639
  const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
41496
42640
  const DETOX_ELEMENT_ACTIONS = new Set([
41497
42641
  "tap",
@@ -41519,7 +42663,8 @@ const PROMISE_SETTLE_METHODS = new Set([
41519
42663
  "catch",
41520
42664
  "finally"
41521
42665
  ]);
41522
- const findChainRoot = (node) => {
42666
+ const findChainRoot = (wrappedNode) => {
42667
+ const node = stripParenExpression(wrappedNode);
41523
42668
  if (isNodeOfType(node, "CallExpression")) {
41524
42669
  if (isNodeOfType(node.callee, "Identifier")) return {
41525
42670
  calleeName: node.callee.name,
@@ -41551,7 +42696,7 @@ const rnDetoxMissingAwait = defineRule({
41551
42696
  recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
41552
42697
  create: (context) => {
41553
42698
  const filename = normalizeFilename(context.filename ?? "");
41554
- if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
42699
+ if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
41555
42700
  return { ExpressionStatement(node) {
41556
42701
  const expression = node.expression;
41557
42702
  if (!isNodeOfType(expression, "CallExpression")) return;
@@ -42107,20 +43252,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
42107
43252
  };
42108
43253
  const isReactNativeDimensionsCallee = (node, callee) => {
42109
43254
  if (!isNodeOfType(callee, "MemberExpression")) return false;
42110
- if (isNodeOfType(callee.object, "Identifier")) {
42111
- const localName = callee.object.name;
43255
+ const receiver = stripParenExpression(callee.object);
43256
+ if (isNodeOfType(receiver, "Identifier")) {
43257
+ const localName = receiver.name;
42112
43258
  const binding = findVariableInitializer(node, localName);
42113
43259
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
42114
43260
  return localName === "Dimensions";
42115
43261
  }
42116
- if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
42117
- if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
42118
- const rootName = getRootIdentifierName(callee.object.object);
43262
+ if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
43263
+ if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
43264
+ const rootName = getRootIdentifierName(receiver.object);
42119
43265
  if (rootName === null) return false;
42120
43266
  const importBinding = getImportBindingForName(node, rootName);
42121
43267
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
42122
43268
  }
42123
- if (getRequireCallSource(callee.object) === REACT_NATIVE_MODULE) return isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions";
43269
+ if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42124
43270
  return false;
42125
43271
  };
42126
43272
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -42526,7 +43672,7 @@ const isLegacyArchReactNativeFile = (filename) => {
42526
43672
  };
42527
43673
  //#endregion
42528
43674
  //#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
42529
- const EMPTY_VISITORS$3 = {};
43675
+ const EMPTY_VISITORS$4 = {};
42530
43676
  const reportLegacyShadowProperties = (objectExpression, context) => {
42531
43677
  const legacyShadowPropertyNames = [];
42532
43678
  for (const property of objectExpression.properties ?? []) {
@@ -42549,7 +43695,7 @@ const rnNoLegacyShadowStyles = defineRule({
42549
43695
  severity: "warn",
42550
43696
  recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
42551
43697
  create: (context) => {
42552
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
43698
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
42553
43699
  return {
42554
43700
  JSXAttribute(node) {
42555
43701
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -42564,7 +43710,8 @@ const rnNoLegacyShadowStyles = defineRule({
42564
43710
  },
42565
43711
  CallExpression(node) {
42566
43712
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
42567
- if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
43713
+ const receiver = stripParenExpression(node.callee.object);
43714
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42568
43715
  if (!isMemberProperty(node.callee, "create")) return;
42569
43716
  const stylesArgument = node.arguments?.[0];
42570
43717
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -43414,7 +44561,7 @@ const rnNoSetNativeProps = defineRule({
43414
44561
  const callee = node.callee;
43415
44562
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
43416
44563
  if (!isNodeOfType(callee, "MemberExpression")) return;
43417
- if (!isStaticMemberNamed(callee.object, "current")) return;
44564
+ if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43418
44565
  context.report({
43419
44566
  node,
43420
44567
  message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
@@ -43470,7 +44617,7 @@ const isExpoManagedFileActive = (context) => {
43470
44617
  };
43471
44618
  //#endregion
43472
44619
  //#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
43473
- const EMPTY_VISITORS$2 = {};
44620
+ const EMPTY_VISITORS$3 = {};
43474
44621
  const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
43475
44622
  const MEMBER_PATH_FAN_OUT_LIMIT = 32;
43476
44623
  const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
@@ -43504,7 +44651,7 @@ const rnPreferExpoImage = defineRule({
43504
44651
  severity: "warn",
43505
44652
  recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
43506
44653
  create: (context) => {
43507
- if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
44654
+ if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
43508
44655
  const flaggedImports = [];
43509
44656
  const assetBindingNames = /* @__PURE__ */ new Set();
43510
44657
  const moduleObjectLiterals = /* @__PURE__ */ new Map();
@@ -43642,14 +44789,15 @@ const analyzeGestureChain = (expression) => {
43642
44789
  if (!isNodeOfType(callee, "MemberExpression")) return null;
43643
44790
  if (!isNodeOfType(callee.property, "Identifier")) return null;
43644
44791
  const methodName = callee.property.name;
43645
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
44792
+ const receiver = stripParenExpression(callee.object);
44793
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43646
44794
  factoryName: methodName,
43647
44795
  chainMethodNames,
43648
44796
  numberOfTapsArgument
43649
44797
  };
43650
44798
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
43651
44799
  chainMethodNames.push(methodName);
43652
- cursor = callee.object;
44800
+ cursor = receiver;
43653
44801
  }
43654
44802
  return null;
43655
44803
  };
@@ -43991,7 +45139,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
43991
45139
  });
43992
45140
  //#endregion
43993
45141
  //#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
43994
- const EMPTY_VISITORS$1 = {};
45142
+ const EMPTY_VISITORS$2 = {};
43995
45143
  const IOS_SHADOW_KEYS = new Set([
43996
45144
  "shadowColor",
43997
45145
  "shadowOffset",
@@ -44077,7 +45225,7 @@ const rnStylePreferBoxShadow = defineRule({
44077
45225
  severity: "warn",
44078
45226
  recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
44079
45227
  create: (context) => {
44080
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
45228
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
44081
45229
  return {
44082
45230
  JSXAttribute(node) {
44083
45231
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -44174,9 +45322,9 @@ const roleHasRequiredAriaProps = defineRule({
44174
45322
  if (!HTML_TAGS.has(elementType)) return;
44175
45323
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
44176
45324
  if (!roleAttribute) return;
44177
- const roleValue = getJsxPropStringValue(roleAttribute);
44178
- if (roleValue === null) return;
44179
- const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
45325
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
45326
+ if (roleCandidates === null) return;
45327
+ const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
44180
45328
  for (const role of roles) {
44181
45329
  const required = ROLE_REQUIRED_PROPS.get(role);
44182
45330
  if (!required) continue;
@@ -47293,7 +48441,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
47293
48441
  };
47294
48442
  //#endregion
47295
48443
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
47296
- const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
48444
+ const buildMessageDefault = (roles, propName) => {
48445
+ return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
48446
+ };
47297
48447
  const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
47298
48448
  const roleSupportsAriaProps = defineRule({
47299
48449
  id: "role-supports-aria-props",
@@ -47313,6 +48463,8 @@ const roleSupportsAriaProps = defineRule({
47313
48463
  const propName = propRawName.toLowerCase();
47314
48464
  if (!propName.startsWith("aria-")) continue;
47315
48465
  if (!ARIA_PROPERTIES.has(propName)) continue;
48466
+ const attributeValue = attributeNode.value;
48467
+ if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
47316
48468
  (ariaAttributes ??= []).push({
47317
48469
  attribute,
47318
48470
  propName
@@ -47321,17 +48473,21 @@ const roleSupportsAriaProps = defineRule({
47321
48473
  if (!ariaAttributes) return;
47322
48474
  const elementType = getElementType(node, context.settings);
47323
48475
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
47324
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
47325
- if (!role) return;
47326
- if (!VALID_ARIA_ROLES.has(role)) return;
48476
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
48477
+ if (roleCandidates === null || roleCandidates.length === 0) return;
48478
+ const supportedSets = [];
48479
+ for (const role of roleCandidates) {
48480
+ if (!VALID_ARIA_ROLES.has(role)) return;
48481
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
48482
+ if (!supported) return;
48483
+ supportedSets.push(supported);
48484
+ }
47327
48485
  const isImplicit = !roleAttribute;
47328
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47329
- if (!supported) return;
47330
48486
  for (const { attribute, propName } of ariaAttributes) {
47331
- if (supported.has(propName)) continue;
48487
+ if (supportedSets.some((supported) => supported.has(propName))) continue;
47332
48488
  context.report({
47333
48489
  node: attribute,
47334
- message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
48490
+ message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47335
48491
  });
47336
48492
  }
47337
48493
  } })
@@ -47678,17 +48834,6 @@ const isInsideLoop = (descendant, ancestor) => {
47678
48834
  }
47679
48835
  return false;
47680
48836
  };
47681
- const isUseEffectEventSymbol = (symbol) => {
47682
- const initializer = symbol.initializer;
47683
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47684
- return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47685
- };
47686
- const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
47687
- const isNonReactEffectEventSymbol = (symbol, contextNode) => {
47688
- const initializer = symbol.initializer;
47689
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47690
- return isNonReactEffectEventCallee(initializer.callee, contextNode);
47691
- };
47692
48837
  const findEnclosingComponentOrHookFunction = (node) => {
47693
48838
  let current = node.parent;
47694
48839
  while (current) {
@@ -47766,7 +48911,7 @@ const rulesOfHooks = defineRule({
47766
48911
  const hookContext = isHookCall$1(node, context.scopes, settings);
47767
48912
  if (!hookContext) return;
47768
48913
  const { hookName } = hookContext;
47769
- if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
48914
+ if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
47770
48915
  context.report({
47771
48916
  node: node.callee,
47772
48917
  message: buildEffectEventPassedDownMessage()
@@ -47878,8 +49023,7 @@ const rulesOfHooks = defineRule({
47878
49023
  },
47879
49024
  Identifier(node) {
47880
49025
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47881
- if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47882
- if (isNonReactEffectEventSymbol(symbol, node)) return;
49026
+ if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
47883
49027
  if (!isSameComponentOrHookScope(symbol, node)) return;
47884
49028
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47885
49029
  context.report({
@@ -48027,7 +49171,8 @@ const serverAfterNonblocking = defineRule({
48027
49171
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48028
49172
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48029
49173
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48030
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
49174
+ const receiver = stripParenExpression(node.callee.object);
49175
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48031
49176
  if (!objectName) return;
48032
49177
  const methodName = node.callee.property.name;
48033
49178
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -48703,7 +49848,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
48703
49848
  };
48704
49849
  const ascendMemberChain = (referenceIdentifier) => {
48705
49850
  let chainTip = referenceIdentifier;
48706
- while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
49851
+ while (chainTip.parent) {
49852
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
49853
+ chainTip = chainTip.parent;
49854
+ continue;
49855
+ }
49856
+ if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
49857
+ chainTip = chainTip.parent;
49858
+ continue;
49859
+ }
49860
+ break;
49861
+ }
48707
49862
  return chainTip;
48708
49863
  };
48709
49864
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -48728,7 +49883,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
48728
49883
  const callee = callExpression.callee;
48729
49884
  if (isNodeOfType(callee, "MemberExpression")) {
48730
49885
  const methodName = getMemberPropertyName$1(callee);
48731
- return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
49886
+ const calleeReceiver = stripParenExpression(callee.object);
49887
+ return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
48732
49888
  }
48733
49889
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
48734
49890
  const calleeSymbol = scopes.symbolFor(callee);
@@ -49458,7 +50614,7 @@ const walkServerFnChain = (outerNode) => {
49458
50614
  };
49459
50615
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
49460
50616
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
49461
- let currentNode = outerNode.callee.object;
50617
+ let currentNode = stripParenExpression(outerNode.callee.object);
49462
50618
  while (isNodeOfType(currentNode, "CallExpression")) {
49463
50619
  const calleeName = getCalleeName$2(currentNode);
49464
50620
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -49469,7 +50625,7 @@ const walkServerFnChain = (outerNode) => {
49469
50625
  }
49470
50626
  }
49471
50627
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
49472
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
50628
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49473
50629
  else break;
49474
50630
  }
49475
50631
  return result;
@@ -50122,7 +51278,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
50122
51278
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
50123
51279
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
50124
51280
  if (methodName) methodNames.unshift(methodName);
50125
- currentNode = currentNode.callee.object;
51281
+ currentNode = stripParenExpression(currentNode.callee.object);
50126
51282
  }
50127
51283
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
50128
51284
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -53204,6 +54360,18 @@ const reactDoctorRules = [
53204
54360
  category: "Bugs"
53205
54361
  }
53206
54362
  },
54363
+ {
54364
+ key: "react-doctor/no-locale-format-in-render",
54365
+ id: "no-locale-format-in-render",
54366
+ source: "react-doctor",
54367
+ originallyExternal: false,
54368
+ rule: {
54369
+ ...noLocaleFormatInRender,
54370
+ framework: "global",
54371
+ category: "Bugs",
54372
+ requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
54373
+ }
54374
+ },
53207
54375
  {
53208
54376
  key: "react-doctor/no-long-transition-duration",
53209
54377
  id: "no-long-transition-duration",
@@ -53632,6 +54800,18 @@ const reactDoctorRules = [
53632
54800
  category: "Maintainability"
53633
54801
  }
53634
54802
  },
54803
+ {
54804
+ key: "react-doctor/no-stale-timer-ref",
54805
+ id: "no-stale-timer-ref",
54806
+ source: "react-doctor",
54807
+ originallyExternal: false,
54808
+ rule: {
54809
+ ...noStaleTimerRef,
54810
+ framework: "global",
54811
+ category: "Bugs",
54812
+ requires: [...new Set(["react", ...noStaleTimerRef.requires ?? []])]
54813
+ }
54814
+ },
53635
54815
  {
53636
54816
  key: "react-doctor/no-static-element-interactions",
53637
54817
  id: "no-static-element-interactions",
@@ -55451,6 +56631,34 @@ const reactDoctorRules = [
55451
56631
  ];
55452
56632
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
55453
56633
  //#endregion
56634
+ //#region src/plugin/utils/is-next-file.ts
56635
+ const isNextFileActive = (context) => {
56636
+ const rawFilename = context.filename;
56637
+ if (!rawFilename) return true;
56638
+ const filename = normalizeFilename(rawFilename);
56639
+ const manifest = readNearestPackageManifest(filename);
56640
+ if (!manifest) return true;
56641
+ if (declaresDependency(manifest, "next")) return true;
56642
+ if (!declaresAnyDependency(manifest)) return true;
56643
+ const packageDirectory = findNearestPackageDirectory(filename);
56644
+ const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
56645
+ if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
56646
+ return true;
56647
+ };
56648
+ //#endregion
56649
+ //#region src/plugin/utils/wrap-nextjs-rule.ts
56650
+ const EMPTY_VISITORS$1 = {};
56651
+ const wrapNextjsRule = (rule) => {
56652
+ const innerCreate = rule.create.bind(rule);
56653
+ return {
56654
+ ...rule,
56655
+ create: (context) => {
56656
+ if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
56657
+ return innerCreate(context);
56658
+ }
56659
+ };
56660
+ };
56661
+ //#endregion
55454
56662
  //#region src/plugin/utils/wrap-react-native-rule.ts
55455
56663
  const EMPTY_VISITORS = {};
55456
56664
  const wrapReactNativeRule = (rule) => {
@@ -55920,9 +57128,14 @@ const wrapWithSemanticContext = (rule) => ({
55920
57128
  });
55921
57129
  //#endregion
55922
57130
  //#region src/plugin/react-doctor-plugin.ts
57131
+ const applyFrameworkGate = (rule) => {
57132
+ if (rule.framework === "react-native") return wrapReactNativeRule(rule);
57133
+ if (rule.framework === "nextjs") return wrapNextjsRule(rule);
57134
+ return rule;
57135
+ };
55923
57136
  const applyFrameworkRuleWrappers = (registry) => {
55924
57137
  const wrapped = {};
55925
- for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
57138
+ for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
55926
57139
  return wrapped;
55927
57140
  };
55928
57141
  const plugin = {
@@ -56047,6 +57260,7 @@ const CROSS_FILE_RULE_IDS = new Set([
56047
57260
  "nextjs-no-use-search-params-without-suspense",
56048
57261
  "no-dynamic-import-path",
56049
57262
  "no-full-lodash-import",
57263
+ "no-locale-format-in-render",
56050
57264
  "no-mutating-reducer-state",
56051
57265
  "prefer-dynamic-import",
56052
57266
  "rendering-hydration-mismatch-time",
@@ -56181,6 +57395,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
56181
57395
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
56182
57396
  ["no-dynamic-import-path", collectNearestManifestDependencies],
56183
57397
  ["no-full-lodash-import", collectNearestManifestDependencies],
57398
+ ["no-locale-format-in-render", collectNearestManifestDependencies],
56184
57399
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
56185
57400
  ["prefer-dynamic-import", collectNearestManifestDependencies],
56186
57401
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],