oxlint-plugin-react-doctor 0.7.2-dev.cb8f726 → 0.7.2-dev.e872767

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 +46 -0
  2. package/dist/index.js +1211 -391
  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",
@@ -687,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
687
693
  "unwatch",
688
694
  "unlisten",
689
695
  "unsub",
690
- "abort"
696
+ "abort",
697
+ "disconnect",
698
+ "unobserve",
699
+ "close"
691
700
  ]);
692
701
  const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
693
702
  "remove",
@@ -2268,6 +2277,7 @@ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2268
2277
  if (remainingHops === 0) return null;
2269
2278
  const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2270
2279
  if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2280
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
2271
2281
  return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
2272
2282
  }
2273
2283
  return null;
@@ -2307,7 +2317,8 @@ const isDirectChildOfLinkComponent = (openingElement) => {
2307
2317
  const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
2308
2318
  const isInvalidHref = (value, validHrefs) => {
2309
2319
  if (validHrefs.has(value)) return false;
2310
- return value === "" || value === "#" || value === "javascript:void(0)";
2320
+ const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
2321
+ return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
2311
2322
  };
2312
2323
  const isNullishOrFragmentHref = (value) => {
2313
2324
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -5182,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
5182
5193
  const callee = node.callee;
5183
5194
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
5184
5195
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
5185
- if (!isWebStorageObject(callee.object)) return;
5196
+ if (!isWebStorageObject(stripParenExpression(callee.object))) return;
5186
5197
  const keyArgument = node.arguments?.[0];
5187
5198
  if (!keyArgument) return;
5188
5199
  const keyString = resolveStaticKeyString(keyArgument);
@@ -6122,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
6122
6133
  })
6123
6134
  });
6124
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
6125
6145
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6126
6146
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6127
6147
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6128
6148
  const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6129
6149
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6130
- const isJsonStringifyCall = (node) => {
6131
- if (!isNodeOfType(node, "CallExpression")) return false;
6132
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
6133
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
6134
- if (node.callee.object.name !== "JSON") return false;
6135
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
6136
- return node.callee.property.name === "stringify";
6137
- };
6150
+ const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6138
6151
  const resolveStringKey = (keyArg, context) => {
6139
6152
  if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
6140
6153
  if (!isNodeOfType(keyArg, "Identifier")) return null;
@@ -6153,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
6153
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.",
6154
6167
  create: (context) => ({ CallExpression(node) {
6155
6168
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6156
- if (!isNodeOfType(node.callee.object, "Identifier")) return;
6157
- 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;
6158
6172
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6159
6173
  if (node.callee.property.name !== "setItem") return;
6160
6174
  const keyArg = node.arguments?.[0];
@@ -6167,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
6167
6181
  if (!isJsonStringifyCall(valueArg)) return;
6168
6182
  context.report({
6169
6183
  node: keyArg,
6170
- 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").`
6171
6185
  });
6172
6186
  } })
6173
6187
  });
@@ -7648,6 +7662,96 @@ const displayName = defineRule({
7648
7662
  }
7649
7663
  });
7650
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/is-result-discarded-call.ts
7721
+ const isResultDiscardedCall = (callExpression) => {
7722
+ let node = callExpression;
7723
+ let parent = node.parent;
7724
+ while (parent) {
7725
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
7726
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
7727
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
7728
+ if (isNodeOfType(parent, "ChainExpression")) {
7729
+ node = parent;
7730
+ parent = node.parent;
7731
+ continue;
7732
+ }
7733
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
7734
+ node = parent;
7735
+ parent = node.parent;
7736
+ continue;
7737
+ }
7738
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
7739
+ node = parent;
7740
+ parent = node.parent;
7741
+ continue;
7742
+ }
7743
+ if (isNodeOfType(parent, "SequenceExpression")) {
7744
+ const expressions = parent.expressions ?? [];
7745
+ if (expressions[expressions.length - 1] !== node) return true;
7746
+ node = parent;
7747
+ parent = node.parent;
7748
+ continue;
7749
+ }
7750
+ return false;
7751
+ }
7752
+ return false;
7753
+ };
7754
+ //#endregion
7651
7755
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7652
7756
  const walkInsideStatementBlocks = (node, visitor) => {
7653
7757
  if (!node || typeof node !== "object") return;
@@ -7799,6 +7903,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7799
7903
  };
7800
7904
  //#endregion
7801
7905
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
7906
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
7907
+ const RESOURCE_NOUN_BY_KIND = {
7908
+ subscribe: "subscription",
7909
+ timer: "timer",
7910
+ socket: "connection"
7911
+ };
7912
+ const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
7913
+ const isSubscribeOrObserveCall = (node) => {
7914
+ if (isSubscribeLikeCallExpression(node)) return true;
7915
+ return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
7916
+ };
7802
7917
  const findSubscribeLikeUsages = (callback) => {
7803
7918
  const usages = [];
7804
7919
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7810,6 +7925,14 @@ const findSubscribeLikeUsages = (callback) => {
7810
7925
  }
7811
7926
  walkAst(callback, (child) => {
7812
7927
  if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
7928
+ if (isSocketConstruction(child)) {
7929
+ usages.push({
7930
+ kind: "socket",
7931
+ node: child,
7932
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
7933
+ });
7934
+ return;
7935
+ }
7813
7936
  if (!isNodeOfType(child, "CallExpression")) return;
7814
7937
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7815
7938
  usages.push({
@@ -7819,7 +7942,7 @@ const findSubscribeLikeUsages = (callback) => {
7819
7942
  });
7820
7943
  return;
7821
7944
  }
7822
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
7945
+ 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({
7823
7946
  kind: "subscribe",
7824
7947
  node: child,
7825
7948
  resourceName: child.callee.property.name
@@ -7835,6 +7958,13 @@ const collectCleanupBindings = (effectCallback) => {
7835
7958
  };
7836
7959
  if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
7837
7960
  if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
7961
+ walkAst(effectCallback.body, (child) => {
7962
+ if (!isSubscribeOrObserveCall(child)) return;
7963
+ if (!isNodeOfType(child, "CallExpression")) return;
7964
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
7965
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
7966
+ bindings.subscriptionNames.add(child.callee.object.name);
7967
+ });
7838
7968
  walkInsideStatementBlocks(effectCallback.body, (child) => {
7839
7969
  if (!isNodeOfType(child, "VariableDeclaration")) return;
7840
7970
  for (const declarator of child.declarations ?? []) {
@@ -7842,7 +7972,12 @@ const collectCleanupBindings = (effectCallback) => {
7842
7972
  const bindingName = declarator.id.name;
7843
7973
  bindings.effectScopeVariableNames.add(bindingName);
7844
7974
  const init = declarator.init;
7845
- if (!init || !isNodeOfType(init, "CallExpression")) continue;
7975
+ if (!init) continue;
7976
+ if (isSocketConstruction(init)) {
7977
+ bindings.subscriptionNames.add(bindingName);
7978
+ continue;
7979
+ }
7980
+ if (!isNodeOfType(init, "CallExpression")) continue;
7846
7981
  if (isSubscribeLikeCallExpression(init)) {
7847
7982
  bindings.subscriptionNames.add(bindingName);
7848
7983
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -7872,6 +8007,22 @@ const getRangeStart = (node) => {
7872
8007
  const rangeStart = node.range?.[0];
7873
8008
  return typeof rangeStart === "number" ? rangeStart : null;
7874
8009
  };
8010
+ const removeSynchronouslyReleasedUsages = (callback, usages) => {
8011
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8012
+ if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8013
+ const releaseStarts = [];
8014
+ walkInsideStatementBlocks(callback.body, (child) => {
8015
+ if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8016
+ const releaseStart = getRangeStart(child);
8017
+ if (releaseStart !== null) releaseStarts.push(releaseStart);
8018
+ });
8019
+ if (releaseStarts.length === 0) return usages;
8020
+ return usages.filter((usage) => {
8021
+ const usageStart = getRangeStart(usage.node);
8022
+ if (usageStart === null) return true;
8023
+ return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8024
+ });
8025
+ };
7875
8026
  const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
7876
8027
  if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
7877
8028
  const returnStart = getRangeStart(returnStatement);
@@ -7894,26 +8045,177 @@ const effectHasCleanupReturn = (callback, usages) => {
7894
8045
  });
7895
8046
  return didFindCleanupReturn;
7896
8047
  };
8048
+ const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8049
+ const isSelfReleasingListenerOptionProperty = (property) => {
8050
+ if (!isNodeOfType(property, "Property")) return false;
8051
+ const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
8052
+ if (keyName === "signal") return true;
8053
+ if (keyName !== "once") return false;
8054
+ return isNodeOfType(property.value, "Literal") && property.value.value === true;
8055
+ };
8056
+ const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
8057
+ const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8058
+ ["addEventListener", new Set(["removeEventListener", "abort"])],
8059
+ ["addListener", new Set([
8060
+ "removeListener",
8061
+ "off",
8062
+ "abort"
8063
+ ])],
8064
+ ["on", new Set([
8065
+ "off",
8066
+ "removeListener",
8067
+ "on"
8068
+ ])],
8069
+ ["subscribe", new Set(["unsubscribe", "unsub"])],
8070
+ ["sub", new Set(["unsub", "unsubscribe"])],
8071
+ ["watch", new Set(["unwatch", "close"])],
8072
+ ["listen", new Set(["unlisten", "close"])],
8073
+ [OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
8074
+ ]);
8075
+ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8076
+ "cleanup",
8077
+ "dispose",
8078
+ "destroy",
8079
+ "teardown"
8080
+ ]);
8081
+ const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8082
+ const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8083
+ const getReleaseVerbName = (node) => {
8084
+ if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8085
+ const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8086
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8087
+ const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8088
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
8089
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
8090
+ return null;
8091
+ };
8092
+ const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8093
+ const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8094
+ let didFindPairedRelease = false;
8095
+ walkAst(body, (child) => {
8096
+ if (didFindPairedRelease) return false;
8097
+ if (child !== body && isFunctionLike$1(child)) return false;
8098
+ const releaseVerbName = getReleaseVerbName(child);
8099
+ if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8100
+ didFindPairedRelease = true;
8101
+ return false;
8102
+ }
8103
+ });
8104
+ return didFindPairedRelease;
8105
+ };
8106
+ const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8107
+ const collectFileReleaseVerbNames = (anyNode) => {
8108
+ let programNode = anyNode;
8109
+ while (programNode.parent) programNode = programNode.parent;
8110
+ const cached = fileReleaseVerbNamesCache.get(programNode);
8111
+ if (cached) return cached;
8112
+ const releaseVerbNames = /* @__PURE__ */ new Set();
8113
+ walkAst(programNode, (child) => {
8114
+ const releaseVerbName = getReleaseVerbName(child);
8115
+ if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
8116
+ });
8117
+ fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8118
+ return releaseVerbNames;
8119
+ };
8120
+ const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8121
+ const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8122
+ const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8123
+ if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8124
+ for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
8125
+ return false;
8126
+ };
8127
+ const findRetainedFunctionLeak = (retainedFunction) => {
8128
+ if (!isFunctionLike$1(retainedFunction)) return null;
8129
+ const body = retainedFunction.body;
8130
+ if (!body) return null;
8131
+ const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8132
+ let leak = null;
8133
+ walkAst(body, (child) => {
8134
+ if (leak !== null) return false;
8135
+ if (isFunctionLike$1(child)) return false;
8136
+ if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8137
+ if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8138
+ leak = {
8139
+ kind: "socket",
8140
+ node: child,
8141
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8142
+ };
8143
+ return false;
8144
+ }
8145
+ if (!isNodeOfType(child, "CallExpression")) return;
8146
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8147
+ leak = {
8148
+ kind: "timer",
8149
+ node: child,
8150
+ resourceName: "setInterval"
8151
+ };
8152
+ return false;
8153
+ }
8154
+ if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8155
+ const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8156
+ if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
8157
+ kind: "subscribe",
8158
+ node: child,
8159
+ resourceName: registrationVerbName
8160
+ };
8161
+ return false;
8162
+ }
8163
+ });
8164
+ return leak;
8165
+ };
8166
+ const isRetainedComponentScopeFunction = (functionNode) => {
8167
+ if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
8168
+ if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
8169
+ if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8170
+ return enclosingComponentOrHookName(functionNode) !== null;
8171
+ };
7897
8172
  const effectNeedsCleanup = defineRule({
7898
8173
  id: "effect-needs-cleanup",
7899
8174
  title: "Effect subscription or timer never cleaned up",
7900
8175
  severity: "error",
7901
8176
  tags: ["test-noise"],
7902
- 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.",
7903
- create: (context) => ({ CallExpression(node) {
7904
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
7905
- const callback = getEffectCallback(node);
7906
- if (!callback) return;
7907
- const usages = findSubscribeLikeUsages(callback);
7908
- if (usages.length === 0) return;
7909
- if (effectHasCleanupReturn(callback, usages)) return;
7910
- const firstUsage = usages[0];
7911
- const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
7912
- context.report({
7913
- node,
7914
- message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
7915
- });
7916
- } })
8177
+ 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.",
8178
+ create: (context) => {
8179
+ const reportRetainedLeak = (retainedFunction) => {
8180
+ const leak = findRetainedFunctionLeak(retainedFunction);
8181
+ if (!leak) return;
8182
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8183
+ context.report({
8184
+ node: leak.node,
8185
+ 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.`
8186
+ });
8187
+ };
8188
+ return {
8189
+ CallExpression(node) {
8190
+ if (isHookCall$2(node, "useCallback")) {
8191
+ const retainedCallback = getEffectCallback(node);
8192
+ if (retainedCallback) reportRetainedLeak(retainedCallback);
8193
+ return;
8194
+ }
8195
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
8196
+ const callback = getEffectCallback(node);
8197
+ if (!callback) return;
8198
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
8199
+ if (usages.length === 0) return;
8200
+ if (effectHasCleanupReturn(callback, usages)) return;
8201
+ const firstUsage = usages[0];
8202
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
8203
+ context.report({
8204
+ node,
8205
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8206
+ });
8207
+ },
8208
+ FunctionDeclaration(node) {
8209
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8210
+ },
8211
+ ArrowFunctionExpression(node) {
8212
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8213
+ },
8214
+ FunctionExpression(node) {
8215
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8216
+ }
8217
+ };
8218
+ }
7917
8219
  });
7918
8220
  //#endregion
7919
8221
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8472,17 +8774,6 @@ const closureCaptures = (functionNode, scopes) => {
8472
8774
  return computedCaptures;
8473
8775
  };
8474
8776
  //#endregion
8475
- //#region src/plugin/utils/is-react-hook-name.ts
8476
- const isReactHookName = (name) => {
8477
- if (!name.startsWith("use")) return false;
8478
- if (name.length === 3) return true;
8479
- const fourthCharacter = name.charCodeAt(3);
8480
- return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8481
- };
8482
- //#endregion
8483
- //#region src/plugin/utils/is-react-component-or-hook-name.ts
8484
- const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8485
- //#endregion
8486
8777
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8487
8778
  const reactHocCalleeName = (callee) => {
8488
8779
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -9919,7 +10210,10 @@ const PRAGMA = "React";
9919
10210
  const isReactFunctionCall = (node, expectedCall) => {
9920
10211
  if (!isNodeOfType(node, "CallExpression")) return false;
9921
10212
  if (getCalleeName$2(node) !== expectedCall) return false;
9922
- if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10213
+ if (isNodeOfType(node.callee, "MemberExpression")) {
10214
+ const receiver = stripParenExpression(node.callee.object);
10215
+ return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10216
+ }
9923
10217
  return true;
9924
10218
  };
9925
10219
  //#endregion
@@ -11297,16 +11591,6 @@ const collectHandlerReferencedNames = (root) => {
11297
11591
  return names;
11298
11592
  };
11299
11593
  //#endregion
11300
- //#region src/plugin/utils/find-enclosing-function.ts
11301
- const findEnclosingFunction = (node) => {
11302
- let cursor = node.parent;
11303
- while (cursor) {
11304
- if (isFunctionLike$1(cursor)) return cursor;
11305
- cursor = cursor.parent ?? null;
11306
- }
11307
- return null;
11308
- };
11309
- //#endregion
11310
11594
  //#region src/plugin/utils/get-function-binding-name.ts
11311
11595
  const getFunctionBindingIdentifier = (functionNode) => {
11312
11596
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -12006,14 +12290,15 @@ const jsCacheStorage = defineRule({
12006
12290
  "ArrowFunctionExpression:exit": exitFunctionScope,
12007
12291
  CallExpression(node) {
12008
12292
  if (!isMemberProperty(node.callee, "getItem")) return;
12009
- if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
12293
+ const receiver = stripParenExpression(node.callee.object);
12294
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
12010
12295
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
12011
12296
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
12012
12297
  const storageKey = String(node.arguments[0].value);
12013
12298
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
12014
12299
  storageReadCounts.set(storageKey, readCount);
12015
12300
  if (readCount === 2) {
12016
- const storageName = node.callee.object.name;
12301
+ const storageName = receiver.name;
12017
12302
  context.report({
12018
12303
  node,
12019
12304
  message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
@@ -12027,13 +12312,16 @@ const jsCacheStorage = defineRule({
12027
12312
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
12028
12313
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
12029
12314
  const callee = callExpression.callee;
12030
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12031
- if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12032
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12033
- const receiver = callee.object;
12034
- if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12035
- return true;
12315
+ if (isNodeOfType(callee, "MemberExpression")) {
12316
+ const receiver = stripParenExpression(callee.object);
12317
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12318
+ if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12319
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12320
+ return true;
12321
+ }
12322
+ return false;
12036
12323
  }
12324
+ if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12037
12325
  return false;
12038
12326
  };
12039
12327
  const isChainPassThroughCall = (callExpression) => {
@@ -12045,10 +12333,7 @@ const isChainPassThroughCall = (callExpression) => {
12045
12333
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12046
12334
  let cursor = receiverNode;
12047
12335
  while (cursor) {
12048
- if (isNodeOfType(cursor, "ChainExpression")) {
12049
- cursor = cursor.expression;
12050
- continue;
12051
- }
12336
+ cursor = stripParenExpression(cursor);
12052
12337
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12053
12338
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12054
12339
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12124,10 +12409,7 @@ const isStringSplitRootedChain = (receiverNode) => {
12124
12409
  let hops = 0;
12125
12410
  while (cursor && hops < 12) {
12126
12411
  hops += 1;
12127
- if (isNodeOfType(cursor, "ChainExpression")) {
12128
- cursor = cursor.expression;
12129
- continue;
12130
- }
12412
+ cursor = stripParenExpression(cursor);
12131
12413
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12132
12414
  const callee = cursor.callee;
12133
12415
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12151,10 +12433,7 @@ const isSmallLiteralArray = (node) => {
12151
12433
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12152
12434
  let cursor = receiverNode;
12153
12435
  while (cursor) {
12154
- if (isNodeOfType(cursor, "ChainExpression")) {
12155
- cursor = cursor.expression;
12156
- continue;
12157
- }
12436
+ cursor = stripParenExpression(cursor);
12158
12437
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12159
12438
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12160
12439
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12219,7 +12498,7 @@ const jsCombineIterations = defineRule({
12219
12498
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12220
12499
  const outerMethod = node.callee.property.name;
12221
12500
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12222
- const innerCall = node.callee.object;
12501
+ const innerCall = stripParenExpression(node.callee.object);
12223
12502
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12224
12503
  const innerMethod = innerCall.callee.property.name;
12225
12504
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12292,10 +12571,10 @@ const jsFlatmapFilter = defineRule({
12292
12571
  if (!filterArgument) return;
12293
12572
  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;
12294
12573
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12295
- const innerCall = node.callee.object;
12574
+ const innerCall = stripParenExpression(node.callee.object);
12296
12575
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12297
12576
  if (innerCall.callee.property.name !== "map") return;
12298
- const receiver = innerCall.callee.object;
12577
+ const receiver = stripParenExpression(innerCall.callee.object);
12299
12578
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12300
12579
  const elements = receiver.elements ?? [];
12301
12580
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13555,7 +13834,7 @@ const jsTosortedImmutable = defineRule({
13555
13834
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13556
13835
  create: (context) => ({ CallExpression(node) {
13557
13836
  if (!isMemberProperty(node.callee, "sort")) return;
13558
- const receiver = node.callee.object;
13837
+ const receiver = stripParenExpression(node.callee.object);
13559
13838
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13560
13839
  const spreadArgument = receiver.elements[0].argument;
13561
13840
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18592,7 +18871,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18592
18871
  };
18593
18872
  const describeClientSideNavigation = (node) => {
18594
18873
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18595
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
18874
+ const receiver = stripParenExpression(node.callee.object);
18875
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18596
18876
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18597
18877
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18598
18878
  }
@@ -19212,7 +19492,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19212
19492
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19213
19493
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19214
19494
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19215
- if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
19495
+ if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19216
19496
  return node.callee.property.name;
19217
19497
  };
19218
19498
  const isMutatingFetchCall = (node) => {
@@ -19226,7 +19506,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19226
19506
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19227
19507
  const { property, object } = node.callee;
19228
19508
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19229
- if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
19509
+ if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19230
19510
  return true;
19231
19511
  };
19232
19512
  const getDbCallDescription = (node) => {
@@ -19234,7 +19514,8 @@ const getDbCallDescription = (node) => {
19234
19514
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19235
19515
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19236
19516
  const methodName = node.callee.property.name;
19237
- const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19517
+ const receiver = stripParenExpression(node.callee.object);
19518
+ const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19238
19519
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19239
19520
  };
19240
19521
  const findSideEffect = (node, options = {}) => {
@@ -20634,13 +20915,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20634
20915
  if (isNodeOfType(current, "CallExpression")) {
20635
20916
  let node = ref.identifier;
20636
20917
  let parent = node.parent;
20637
- while (parent && isNodeOfType(parent, "MemberExpression")) {
20918
+ while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20638
20919
  node = parent;
20639
20920
  parent = node.parent;
20640
20921
  }
20641
20922
  if (current.callee === node) return current;
20642
20923
  }
20643
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
20924
+ if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20644
20925
  return null;
20645
20926
  };
20646
20927
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -20775,7 +21056,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
20775
21056
  const importDeclaration = declarationNode.parent;
20776
21057
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
20777
21058
  }));
20778
- const isHookCallee = (analysis, node, hookName) => {
21059
+ const isHookCallee$1 = (analysis, node, hookName) => {
20779
21060
  if (!node) return false;
20780
21061
  if (isNodeOfType(node, "Identifier")) {
20781
21062
  if (node.name === hookName) return true;
@@ -20784,15 +21065,19 @@ const isHookCallee = (analysis, node, hookName) => {
20784
21065
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
20785
21066
  return false;
20786
21067
  }
20787
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21068
+ if (isNodeOfType(node, "MemberExpression")) {
21069
+ const receiver = stripParenExpression(node.object);
21070
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21071
+ }
20788
21072
  return false;
20789
21073
  };
20790
21074
  const isUseEffect = (node) => {
20791
21075
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
20792
21076
  const callee = node.callee;
20793
21077
  if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
20794
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
20795
- return false;
21078
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
21079
+ const receiver = stripParenExpression(callee.object);
21080
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
20796
21081
  };
20797
21082
  const getEffectFn = (analysis, node) => {
20798
21083
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -20820,7 +21105,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20820
21105
  const node = def.node;
20821
21106
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20822
21107
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20823
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21108
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20824
21109
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20825
21110
  const elements = node.id.elements ?? [];
20826
21111
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -20831,7 +21116,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
20831
21116
  const node = def.node;
20832
21117
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20833
21118
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20834
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21119
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20835
21120
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20836
21121
  const elements = node.id.elements ?? [];
20837
21122
  if (elements.length !== 2) return false;
@@ -20878,7 +21163,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20878
21163
  const node = def.node;
20879
21164
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20880
21165
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20881
- return isHookCallee(analysis, node.init.callee, "useRef");
21166
+ return isHookCallee$1(analysis, node.init.callee, "useRef");
20882
21167
  }));
20883
21168
  const isRefCurrent = (ref) => {
20884
21169
  const parent = ref.identifier.parent;
@@ -20891,11 +21176,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
20891
21176
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
20892
21177
  const isPropCallbackInvocationRef = (analysis, ref) => {
20893
21178
  if (!isPropAlias(analysis, ref)) return false;
20894
- const identifier = ref.identifier;
20895
- const parent = identifier.parent;
21179
+ let effectiveNode = ref.identifier;
21180
+ let parent = effectiveNode.parent;
21181
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21182
+ effectiveNode = parent;
21183
+ parent = effectiveNode.parent;
21184
+ }
20896
21185
  if (!parent) return false;
20897
- if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
20898
- if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21186
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21187
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
20899
21188
  const memberParent = parent.parent;
20900
21189
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
20901
21190
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -20906,7 +21195,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
20906
21195
  };
20907
21196
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
20908
21197
  const getUseStateDecl = (analysis, ref) => {
20909
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21198
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
20910
21199
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
20911
21200
  return node ?? null;
20912
21201
  };
@@ -21111,7 +21400,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21111
21400
  const noAdjustStateOnPropChange = defineRule({
21112
21401
  id: "no-adjust-state-on-prop-change",
21113
21402
  title: "State synced to a prop inside an effect",
21114
- severity: "error",
21403
+ severity: "warn",
21115
21404
  tags: ["test-noise"],
21116
21405
  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",
21117
21406
  create: (context) => ({ CallExpression(node) {
@@ -21152,7 +21441,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21152
21441
  "summary",
21153
21442
  "textarea"
21154
21443
  ]);
21444
+ const isStaticallyFalseBooleanAttribute = (attribute) => {
21445
+ const value = attribute.value;
21446
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
21447
+ const expression = value.expression;
21448
+ return isNodeOfType(expression, "Literal") && expression.value === false;
21449
+ };
21450
+ const DISABLEABLE_TAGS = new Set([
21451
+ "button",
21452
+ "input",
21453
+ "select",
21454
+ "textarea"
21455
+ ]);
21155
21456
  const isNativelyFocusable = (tagName, openingElement) => {
21457
+ if (DISABLEABLE_TAGS.has(tagName)) {
21458
+ const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
21459
+ if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
21460
+ }
21156
21461
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21157
21462
  switch (tagName) {
21158
21463
  case "input": {
@@ -21166,7 +21471,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
21166
21471
  case "a":
21167
21472
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21168
21473
  case "audio":
21169
- case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
21474
+ case "video": {
21475
+ const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
21476
+ return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
21477
+ }
21170
21478
  default: return false;
21171
21479
  }
21172
21480
  };
@@ -22130,7 +22438,8 @@ const SECOND_INDEX_METHODS = new Set([
22130
22438
  "some"
22131
22439
  ]);
22132
22440
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22133
- const isPositionallyStableIterationReceiver = (receiver) => {
22441
+ const isPositionallyStableIterationReceiver = (receiverNode) => {
22442
+ const receiver = stripParenExpression(receiverNode);
22134
22443
  if (isAllLiteralArrayExpression(receiver)) return true;
22135
22444
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22136
22445
  const only = receiver.elements[0];
@@ -22141,17 +22450,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
22141
22450
  }
22142
22451
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22143
22452
  const callee = receiver.callee;
22144
- 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;
22453
+ if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22145
22454
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22146
22455
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22147
22456
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22148
22457
  return false;
22149
22458
  };
22150
- const isArrayFromMapperCallback = (parentCall, callback) => {
22151
- if (parentCall.arguments[1] !== callback) return false;
22152
- const callee = parentCall.callee;
22153
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
22154
- };
22459
+ const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22155
22460
  const isArrayFromSourcePositionallyStable = (source) => {
22156
22461
  if (isNodeOfType(source, "ObjectExpression")) {
22157
22462
  for (const property of source.properties ?? []) {
@@ -22210,18 +22515,12 @@ const expressionUsesIndex = (expression, paramName) => {
22210
22515
  return false;
22211
22516
  }
22212
22517
  if (isNodeOfType(expression, "CallExpression")) {
22213
- if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
22518
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22214
22519
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22215
22520
  }
22216
22521
  return false;
22217
22522
  };
22218
- const isReactCloneElement = (callExpression) => {
22219
- const callee = callExpression.callee;
22220
- if (!isNodeOfType(callee, "MemberExpression")) return false;
22221
- if (!isNodeOfType(callee.property, "Identifier")) return false;
22222
- if (callee.property.name !== "cloneElement") return false;
22223
- return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
22224
- };
22523
+ const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22225
22524
  const noArrayIndexKey = defineRule({
22226
22525
  id: "no-array-index-key",
22227
22526
  title: "Array index used as a key",
@@ -22945,7 +23244,7 @@ const isAsyncFunctionLike = (node) => {
22945
23244
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
22946
23245
  return false;
22947
23246
  };
22948
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23247
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
22949
23248
  "forEach",
22950
23249
  "map",
22951
23250
  "filter",
@@ -22966,30 +23265,51 @@ const runsOnEffectDispatch = (functionNode) => {
22966
23265
  if (parent.callee === functionNode) return true;
22967
23266
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
22968
23267
  const callee = parent.callee;
22969
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
23268
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
22970
23269
  };
22971
23270
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
22972
- const isGuardWithTerminatingBranch = (statement) => {
22973
- if (!isNodeOfType(statement, "IfStatement")) return null;
22974
- if (statement.alternate) return null;
22975
- const consequent = statement.consequent;
22976
- if (isTerminatingStatement(consequent)) return consequent;
22977
- if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
22978
- return null;
22979
- };
22980
- const countStatementSequenceSetStateCalls = (statements, context) => {
23271
+ const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
23272
+ const analyzeStatementSequence = (statements, context) => {
22981
23273
  let fallThroughCount = 0;
22982
- let maxTerminatingPathCount = 0;
23274
+ let maxTerminatedCount = 0;
22983
23275
  for (const statement of statements) {
22984
- const guardBranch = isGuardWithTerminatingBranch(statement);
22985
- if (guardBranch) {
22986
- maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
23276
+ if (isNodeOfType(statement, "IfStatement")) {
23277
+ fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
23278
+ const thenSummary = analyzeBranchStatements(statement.consequent, context);
23279
+ const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
23280
+ fallThroughCount: 0,
23281
+ maxTerminatedCount: 0,
23282
+ doAllPathsTerminate: false
23283
+ };
23284
+ maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
23285
+ if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
23286
+ fallThroughCount: 0,
23287
+ maxTerminatedCount,
23288
+ doAllPathsTerminate: true
23289
+ };
23290
+ const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
23291
+ fallThroughCount += Math.max(...fallThroughBranchCounts);
22987
23292
  continue;
22988
23293
  }
22989
- if (isTerminatingStatement(statement)) break;
23294
+ if (isTerminatingStatement(statement)) {
23295
+ const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
23296
+ return {
23297
+ fallThroughCount: 0,
23298
+ maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
23299
+ doAllPathsTerminate: true
23300
+ };
23301
+ }
22990
23302
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
22991
23303
  }
22992
- return Math.max(maxTerminatingPathCount, fallThroughCount);
23304
+ return {
23305
+ fallThroughCount,
23306
+ maxTerminatedCount,
23307
+ doAllPathsTerminate: false
23308
+ };
23309
+ };
23310
+ const countStatementSequenceSetStateCalls = (statements, context) => {
23311
+ const summary = analyzeStatementSequence(statements, context);
23312
+ return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
22993
23313
  };
22994
23314
  const collectLocalHelperFunctions = (root) => {
22995
23315
  const helpersByName = /* @__PURE__ */ new Map();
@@ -23120,7 +23440,9 @@ const isDevOnlyGuardedEffect = (callback) => {
23120
23440
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23121
23441
  const firstStatement = (body.body ?? [])[0];
23122
23442
  if (!firstStatement) return false;
23123
- if (!isGuardWithTerminatingBranch(firstStatement)) return false;
23443
+ if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
23444
+ const consequent = firstStatement.consequent;
23445
+ if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23124
23446
  return mentionsDevEnvFlag(firstStatement.test);
23125
23447
  };
23126
23448
  const noCascadingSetState = defineRule({
@@ -23479,40 +23801,6 @@ const noCloneElement = defineRule({
23479
23801
  } })
23480
23802
  });
23481
23803
  //#endregion
23482
- //#region src/plugin/utils/component-or-hook-display-name.ts
23483
- const hocWrapperCalleeName = (callee) => {
23484
- if (isNodeOfType(callee, "Identifier")) return callee.name;
23485
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
23486
- return null;
23487
- };
23488
- const displayNameFromFunctionBinding = (functionNode) => {
23489
- let current = functionNode;
23490
- for (;;) {
23491
- const parent = current.parent;
23492
- if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
23493
- const calleeName = hocWrapperCalleeName(parent.callee);
23494
- if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
23495
- current = parent;
23496
- continue;
23497
- }
23498
- }
23499
- break;
23500
- }
23501
- const binding = current.parent;
23502
- if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
23503
- return null;
23504
- };
23505
- const componentOrHookDisplayNameForFunction = (functionNode) => {
23506
- if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
23507
- return displayNameFromFunctionBinding(functionNode);
23508
- };
23509
- //#endregion
23510
- //#region src/plugin/utils/enclosing-component-or-hook-name.ts
23511
- const enclosingComponentOrHookName = (node) => {
23512
- const functionNode = findEnclosingFunction(node);
23513
- return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
23514
- };
23515
- //#endregion
23516
23804
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23517
23805
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23518
23806
  const CONTEXT_MODULES = [
@@ -24483,11 +24771,21 @@ const isInitialOnlySetterCall = (callExpr) => {
24483
24771
  return isInitialOnlyPropName(arg.name);
24484
24772
  };
24485
24773
  //#endregion
24774
+ //#region src/plugin/utils/is-no-op-statement.ts
24775
+ const isNoOpStatement = (statement) => {
24776
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
24777
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24778
+ const expression = stripParenExpression(statement.expression);
24779
+ if (isNodeOfType(expression, "Literal")) return true;
24780
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
24781
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
24782
+ return false;
24783
+ };
24784
+ //#endregion
24486
24785
  //#region src/plugin/utils/get-callback-statements.ts
24487
24786
  const getCallbackStatements = (callback) => {
24488
24787
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
24489
- if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
24490
- return callback.body ? [callback.body] : [];
24788
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24491
24789
  };
24492
24790
  //#endregion
24493
24791
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24526,6 +24824,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24526
24824
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24527
24825
  }
24528
24826
  };
24827
+ const flattenGuardedStatements = (statements) => {
24828
+ const flattened = [];
24829
+ for (const statement of statements) {
24830
+ if (isNoOpStatement(statement)) continue;
24831
+ if (isNodeOfType(statement, "ExpressionStatement")) {
24832
+ flattened.push(statement);
24833
+ continue;
24834
+ }
24835
+ if (isNodeOfType(statement, "IfStatement")) {
24836
+ for (const branch of [statement.consequent, statement.alternate]) {
24837
+ if (!branch) continue;
24838
+ const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
24839
+ if (flattenedBranch === null) return null;
24840
+ flattened.push(...flattenedBranch);
24841
+ }
24842
+ continue;
24843
+ }
24844
+ return null;
24845
+ }
24846
+ return flattened;
24847
+ };
24529
24848
  const noDerivedStateEffect = defineRule({
24530
24849
  id: "no-derived-state-effect",
24531
24850
  title: "Derived state stored in an effect",
@@ -24557,8 +24876,8 @@ const noDerivedStateEffect = defineRule({
24557
24876
  }
24558
24877
  }
24559
24878
  if (sawAnyDep && allDepsAreInitialOnly) return;
24560
- const statements = getCallbackStatements(callback);
24561
- if (statements.length === 0) return;
24879
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
24880
+ if (statements === null || statements.length === 0) return;
24562
24881
  if (!statements.every((statement) => {
24563
24882
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24564
24883
  const expression = statement.expression;
@@ -25193,7 +25512,7 @@ const noDidMountSetState = defineRule({
25193
25512
  const { mode } = resolveSettings$20(context.settings);
25194
25513
  return { CallExpression(node) {
25195
25514
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25196
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25515
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25197
25516
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25198
25517
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25199
25518
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25318,7 +25637,7 @@ const noDidUpdateSetState = defineRule({
25318
25637
  const { mode } = resolveSettings$19(context.settings);
25319
25638
  return { CallExpression(node) {
25320
25639
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25321
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25640
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25322
25641
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25323
25642
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25324
25643
  if (isInsideDiffGuard(node)) return;
@@ -25639,9 +25958,10 @@ const noDocumentStartViewTransition = defineRule({
25639
25958
  create: (context) => ({ CallExpression(node) {
25640
25959
  const callee = node.callee;
25641
25960
  if (!isNodeOfType(callee, "MemberExpression")) return;
25642
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
25961
+ const receiver = stripParenExpression(callee.object);
25962
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25643
25963
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
25644
- if (context.scopes.symbolFor(callee.object) !== null) return;
25964
+ if (context.scopes.symbolFor(receiver) !== null) return;
25645
25965
  if (!importsReactViewTransition(node)) return;
25646
25966
  context.report({
25647
25967
  node,
@@ -25661,7 +25981,8 @@ const noDocumentWrite = defineRule({
25661
25981
  create: (context) => ({ CallExpression(node) {
25662
25982
  const callee = node.callee;
25663
25983
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
25664
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
25984
+ const receiver = stripParenExpression(callee.object);
25985
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25665
25986
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
25666
25987
  context.report({
25667
25988
  node,
@@ -26358,6 +26679,7 @@ const noEffectEventInDeps = defineRule({
26358
26679
  const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26359
26680
  if (calleeSymbol && calleeSymbol.kind !== "import") return;
26360
26681
  }
26682
+ if (isNodeOfType(initializer.callee, "MemberExpression") && !initializer.callee.computed && isNodeOfType(initializer.callee.object, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.object.name)) return;
26361
26683
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26362
26684
  } });
26363
26685
  return {
@@ -28937,7 +29259,7 @@ const noIsMounted = defineRule({
28937
29259
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
28938
29260
  create: (context) => ({ CallExpression(node) {
28939
29261
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
28940
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
29262
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
28941
29263
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
28942
29264
  if (!getParentComponent(node)) return;
28943
29265
  context.report({
@@ -28952,7 +29274,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
28952
29274
  const isJsonMethodCall = (node, method) => {
28953
29275
  if (!isNodeOfType(node, "CallExpression")) return false;
28954
29276
  const callee = node.callee;
28955
- return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
29277
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
29278
+ const receiver = stripParenExpression(callee.object);
29279
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
28956
29280
  };
28957
29281
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
28958
29282
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -29037,7 +29361,7 @@ const extractReturnTypeAnnotation = (returnType) => {
29037
29361
  const noJsxElementType = defineRule({
29038
29362
  id: "no-jsx-element-type",
29039
29363
  title: "No JSX.Element",
29040
- severity: "error",
29364
+ severity: "warn",
29041
29365
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
29042
29366
  create: (context) => {
29043
29367
  let isJsxImported = false;
@@ -29363,6 +29687,378 @@ const noLegacyContextApi = defineRule({
29363
29687
  }
29364
29688
  });
29365
29689
  //#endregion
29690
+ //#region src/plugin/utils/executes-during-render.ts
29691
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29692
+ "map",
29693
+ "filter",
29694
+ "forEach",
29695
+ "flatMap",
29696
+ "reduce",
29697
+ "reduceRight",
29698
+ "some",
29699
+ "every",
29700
+ "find",
29701
+ "findIndex",
29702
+ "findLast",
29703
+ "findLastIndex",
29704
+ "sort",
29705
+ "toSorted"
29706
+ ]);
29707
+ const executesDuringRender = (functionNode) => {
29708
+ const parent = functionNode.parent;
29709
+ if (!isNodeOfType(parent, "CallExpression")) return false;
29710
+ if (parent.callee === functionNode) return true;
29711
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
29712
+ 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;
29713
+ };
29714
+ //#endregion
29715
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29716
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
29717
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29718
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29719
+ return false;
29720
+ };
29721
+ //#endregion
29722
+ //#region src/plugin/utils/find-declarator-for-binding.ts
29723
+ const findDeclaratorForBinding = (bindingIdentifier) => {
29724
+ let ancestor = bindingIdentifier.parent;
29725
+ while (ancestor) {
29726
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29727
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29728
+ ancestor = ancestor.parent ?? null;
29729
+ }
29730
+ return null;
29731
+ };
29732
+ //#endregion
29733
+ //#region src/plugin/utils/references-falsy-initial-state.ts
29734
+ const isFalsyLiteral = (node) => {
29735
+ if (!node) return true;
29736
+ if (isNodeOfType(node, "Literal")) return !node.value;
29737
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
29738
+ };
29739
+ const isFalsyInitialStateBinding = (identifier) => {
29740
+ if (!isNodeOfType(identifier, "Identifier")) return false;
29741
+ const binding = findVariableInitializer(identifier, identifier.name);
29742
+ if (!binding) return false;
29743
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29744
+ if (!declarator?.init) return false;
29745
+ const init = stripParenExpression(declarator.init);
29746
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
29747
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
29748
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
29749
+ return isFalsyLiteral(init.arguments?.[0]);
29750
+ };
29751
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
29752
+ //#endregion
29753
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29754
+ const isGatedByFalsyInitialState = (node) => {
29755
+ let cursor = node;
29756
+ let parent = node.parent;
29757
+ while (parent) {
29758
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
29759
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29760
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29761
+ cursor = parent;
29762
+ parent = parent.parent ?? null;
29763
+ }
29764
+ return false;
29765
+ };
29766
+ //#endregion
29767
+ //#region src/plugin/utils/references-client-only-flag.ts
29768
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
29769
+ const referencesClientOnlyFlag = (expression) => {
29770
+ const unwrapped = stripParenExpression(expression);
29771
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
29772
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
29773
+ const property = unwrapped.property;
29774
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
29775
+ }
29776
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
29777
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
29778
+ return false;
29779
+ };
29780
+ //#endregion
29781
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
29782
+ const isInsideClientOnlyGuard = (node) => {
29783
+ let cursor = node;
29784
+ let parent = node.parent;
29785
+ while (parent) {
29786
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
29787
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
29788
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
29789
+ cursor = parent;
29790
+ parent = parent.parent ?? null;
29791
+ }
29792
+ return false;
29793
+ };
29794
+ //#endregion
29795
+ //#region src/plugin/rules/performance/no-locale-format-in-render.ts
29796
+ const LOCALE_FORMAT_METHOD_NAMES = new Set([
29797
+ "toLocaleString",
29798
+ "toLocaleDateString",
29799
+ "toLocaleTimeString"
29800
+ ]);
29801
+ const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
29802
+ const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
29803
+ const INTL_FORMAT_METHOD_NAMES = new Set([
29804
+ "format",
29805
+ "formatToParts",
29806
+ "formatRange"
29807
+ ]);
29808
+ const isProvableDateExpression = (expression) => {
29809
+ if (!expression) return false;
29810
+ const unwrapped = stripParenExpression(expression);
29811
+ return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
29812
+ };
29813
+ const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
29814
+ const receiverNameLooksDateFlavored = (expression) => {
29815
+ if (!expression) return false;
29816
+ const unwrapped = stripParenExpression(expression);
29817
+ if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
29818
+ if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
29819
+ if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
29820
+ return false;
29821
+ };
29822
+ const objectLiteralHasProperty = (objectExpression, propertyName) => {
29823
+ if (!objectExpression) return false;
29824
+ const unwrapped = stripParenExpression(objectExpression);
29825
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
29826
+ for (const property of unwrapped.properties ?? []) {
29827
+ if (!isNodeOfType(property, "Property")) continue;
29828
+ if (property.computed) continue;
29829
+ if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
29830
+ if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
29831
+ }
29832
+ return false;
29833
+ };
29834
+ const hasExplicitLocaleArgument = (argument) => {
29835
+ if (!argument) return false;
29836
+ const unwrapped = stripParenExpression(argument);
29837
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
29838
+ return true;
29839
+ };
29840
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
29841
+ const localeArgument = call.arguments?.[0];
29842
+ if (!hasExplicitLocaleArgument(localeArgument)) return false;
29843
+ const optionsArgument = call.arguments?.[1];
29844
+ if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
29845
+ return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
29846
+ };
29847
+ const matchLocaleMethodCall = (call) => {
29848
+ const callee = call.callee;
29849
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29850
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29851
+ const methodName = callee.property.name;
29852
+ if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
29853
+ const receiverIsProvablyDate = isProvableDateExpression(callee.object);
29854
+ if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
29855
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
29856
+ return {
29857
+ node: call,
29858
+ display: `${methodName}()`
29859
+ };
29860
+ };
29861
+ const getIntlFormatterName = (expression) => {
29862
+ if (!expression) return null;
29863
+ const unwrapped = stripParenExpression(expression);
29864
+ if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
29865
+ const callee = unwrapped.callee;
29866
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29867
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
29868
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29869
+ return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
29870
+ };
29871
+ const isDeterministicIntlConstruction = (construction, formatterName) => {
29872
+ if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
29873
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
29874
+ if (formatterName !== "DateTimeFormat") return true;
29875
+ return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
29876
+ };
29877
+ const matchIntlFormatCall = (call) => {
29878
+ const callee = call.callee;
29879
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29880
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29881
+ if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
29882
+ let construction = stripParenExpression(callee.object);
29883
+ if (isNodeOfType(construction, "Identifier")) {
29884
+ const binding = findVariableInitializer(construction, construction.name);
29885
+ construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
29886
+ }
29887
+ if (!construction) return null;
29888
+ const formatterName = getIntlFormatterName(construction);
29889
+ if (!formatterName) return null;
29890
+ if (isDeterministicIntlConstruction(construction, formatterName)) return null;
29891
+ return {
29892
+ node: call,
29893
+ display: `Intl.${formatterName}().${callee.property.name}()`
29894
+ };
29895
+ };
29896
+ const isDeterministicInputDateConstruction = (expression) => {
29897
+ if (!expression) return false;
29898
+ const unwrapped = stripParenExpression(expression);
29899
+ if (!isNodeOfType(unwrapped, "NewExpression")) return false;
29900
+ if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
29901
+ return (unwrapped.arguments?.length ?? 0) > 0;
29902
+ };
29903
+ const matchDateDefaultStringification = (node) => {
29904
+ if (isNodeOfType(node, "CallExpression")) {
29905
+ const callee = node.callee;
29906
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
29907
+ node,
29908
+ display: "Date.prototype.toString()"
29909
+ };
29910
+ if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
29911
+ node,
29912
+ display: "String(new Date(…))"
29913
+ };
29914
+ return null;
29915
+ }
29916
+ if (isNodeOfType(node, "TemplateLiteral")) {
29917
+ for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
29918
+ node: expression,
29919
+ display: "`${new Date(…)}`"
29920
+ };
29921
+ }
29922
+ return null;
29923
+ };
29924
+ const findRenderPhaseComponentOrHook = (node) => {
29925
+ let functionNode = findEnclosingFunction(node);
29926
+ while (functionNode) {
29927
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
29928
+ if (!executesDuringRender(functionNode)) return null;
29929
+ functionNode = findEnclosingFunction(functionNode);
29930
+ }
29931
+ return null;
29932
+ };
29933
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
29934
+ if (fileHasUseClientDirective) return true;
29935
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
29936
+ if (displayName && isReactHookName(displayName)) return true;
29937
+ let callsHook = false;
29938
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
29939
+ if (callsHook) return false;
29940
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
29941
+ callsHook = true;
29942
+ return false;
29943
+ }
29944
+ });
29945
+ return callsHook;
29946
+ };
29947
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
29948
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
29949
+ if (!isNodeOfType(body, "BlockStatement")) return false;
29950
+ const ancestors = /* @__PURE__ */ new Set();
29951
+ let cursor = node;
29952
+ while (cursor) {
29953
+ ancestors.add(cursor);
29954
+ cursor = cursor.parent ?? null;
29955
+ }
29956
+ for (const statement of body.body ?? []) {
29957
+ if (ancestors.has(statement)) return false;
29958
+ if (!isNodeOfType(statement, "IfStatement")) continue;
29959
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
29960
+ let returnsEarly = false;
29961
+ walkAst(statement.consequent, (child) => {
29962
+ if (isFunctionLike$1(child)) return false;
29963
+ if (isNodeOfType(child, "ReturnStatement")) {
29964
+ returnsEarly = true;
29965
+ return false;
29966
+ }
29967
+ });
29968
+ if (returnsEarly) return true;
29969
+ }
29970
+ return false;
29971
+ };
29972
+ const findEnclosingJsxOpeningElement = (node) => {
29973
+ let cursor = node.parent;
29974
+ while (cursor) {
29975
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
29976
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
29977
+ cursor = cursor.parent ?? null;
29978
+ }
29979
+ return null;
29980
+ };
29981
+ const noLocaleFormatInRender = defineRule({
29982
+ id: "no-locale-format-in-render",
29983
+ title: "Locale/timezone formatting during render",
29984
+ severity: "warn",
29985
+ category: "Correctness",
29986
+ disabledWhen: [
29987
+ "vite",
29988
+ "cra",
29989
+ "expo",
29990
+ "react-native",
29991
+ "unknown"
29992
+ ],
29993
+ 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.",
29994
+ create: (context) => {
29995
+ if (isTestlikeFilename(context.filename)) return {};
29996
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
29997
+ let fileHasUseClientDirective = false;
29998
+ let fileIsEmailTemplate = false;
29999
+ const reportedNodes = /* @__PURE__ */ new Set();
30000
+ const reportIfRenderPhase = (match) => {
30001
+ if (reportedNodes.has(match.node)) return;
30002
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30003
+ if (!componentOrHookNode) return;
30004
+ if (fileIsEmailTemplate) return;
30005
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30006
+ if (isInsideClientOnlyGuard(match.node)) return;
30007
+ if (isGatedByFalsyInitialState(match.node)) return;
30008
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30009
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30010
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30011
+ reportedNodes.add(match.node);
30012
+ context.report({
30013
+ node: match.node,
30014
+ 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.`
30015
+ });
30016
+ };
30017
+ return {
30018
+ Program(node) {
30019
+ fileHasUseClientDirective = hasDirective(node, "use client");
30020
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30021
+ },
30022
+ CallExpression(node) {
30023
+ const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30024
+ if (match) reportIfRenderPhase(match);
30025
+ },
30026
+ TemplateLiteral(node) {
30027
+ const match = matchDateDefaultStringification(node);
30028
+ if (match) reportIfRenderPhase(match);
30029
+ },
30030
+ JSXExpressionContainer(node) {
30031
+ const expression = stripParenExpression(node.expression);
30032
+ if (!isNodeOfType(expression, "CallExpression")) return;
30033
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
30034
+ const helperName = expression.callee.name;
30035
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30036
+ if (!componentOrHookNode) return;
30037
+ const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30038
+ if (!helperNode || !isFunctionLike$1(helperNode)) return;
30039
+ if (componentOrHookDisplayNameForFunction(helperNode)) return;
30040
+ walkAst(helperNode.body ?? helperNode, (child) => {
30041
+ if (isFunctionLike$1(child)) return false;
30042
+ if (!isNodeOfType(child, "CallExpression")) return;
30043
+ const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30044
+ if (!match || reportedNodes.has(match.node)) return;
30045
+ if (fileIsEmailTemplate) return;
30046
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30047
+ if (isInsideClientOnlyGuard(node)) return;
30048
+ if (isGatedByFalsyInitialState(node)) return;
30049
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30050
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30051
+ reportedNodes.add(match.node);
30052
+ context.report({
30053
+ node: match.node,
30054
+ 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.`
30055
+ });
30056
+ });
30057
+ }
30058
+ };
30059
+ }
30060
+ });
30061
+ //#endregion
29366
30062
  //#region src/plugin/rules/design/no-long-transition-duration.ts
29367
30063
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
29368
30064
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -30176,7 +30872,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30176
30872
  "reverse",
30177
30873
  "sort"
30178
30874
  ]);
30179
- const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30180
30875
  const OBJECT_MUTATION_METHODS = new Set([
30181
30876
  "assign",
30182
30877
  "defineProperties",
@@ -30250,7 +30945,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30250
30945
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30251
30946
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30252
30947
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30253
- if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30254
30948
  }
30255
30949
  }
30256
30950
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30289,6 +30983,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30289
30983
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30290
30984
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30291
30985
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
30986
+ if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
30292
30987
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30293
30988
  });
30294
30989
  return mutations;
@@ -30521,7 +31216,7 @@ const noNestedComponentDefinition = defineRule({
30521
31216
  id: "no-nested-component-definition",
30522
31217
  title: "Component defined inside another component",
30523
31218
  tags: ["test-noise", "react-jsx-only"],
30524
- severity: "error",
31219
+ severity: "warn",
30525
31220
  category: "Correctness",
30526
31221
  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.",
30527
31222
  create: (context) => {
@@ -31476,12 +32171,12 @@ const noPassDataToParent = defineRule({
31476
32171
  if (calleeNode === identifier) {
31477
32172
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
31478
32173
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
31479
- } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32174
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31480
32175
  if (!isWholePropsObjectReference(analysis, ref)) continue;
31481
32176
  if (isCustomHookParameter(ref)) continue;
31482
32177
  } else continue;
31483
32178
  const methodName = getCallMethodName(calleeNode);
31484
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32179
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31485
32180
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31486
32181
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
31487
32182
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -31672,7 +32367,7 @@ const noPassLiveStateToParent = defineRule({
31672
32367
  if (isCallResultConsumedAsArgument(callExpr)) continue;
31673
32368
  const calleeNode = callExpr.callee;
31674
32369
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
31675
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32370
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31676
32371
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31677
32372
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
31678
32373
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -32000,40 +32695,6 @@ const noPreventDefault = defineRule({
32000
32695
  }
32001
32696
  });
32002
32697
  //#endregion
32003
- //#region src/plugin/utils/is-result-discarded-call.ts
32004
- const isResultDiscardedCall = (callExpression) => {
32005
- let node = callExpression;
32006
- let parent = node.parent;
32007
- while (parent) {
32008
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
32009
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
32010
- if (isNodeOfType(parent, "ChainExpression")) {
32011
- node = parent;
32012
- parent = node.parent;
32013
- continue;
32014
- }
32015
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
32016
- node = parent;
32017
- parent = node.parent;
32018
- continue;
32019
- }
32020
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
32021
- node = parent;
32022
- parent = node.parent;
32023
- continue;
32024
- }
32025
- if (isNodeOfType(parent, "SequenceExpression")) {
32026
- const expressions = parent.expressions ?? [];
32027
- if (expressions[expressions.length - 1] !== node) return true;
32028
- node = parent;
32029
- parent = node.parent;
32030
- continue;
32031
- }
32032
- return false;
32033
- }
32034
- return false;
32035
- };
32036
- //#endregion
32037
32698
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
32038
32699
  const isRefLatchGuardedEffect = (callbackBody) => {
32039
32700
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -32240,7 +32901,7 @@ const isAlwaysFreshExpression = (expression) => {
32240
32901
  return `${callee.name}()`;
32241
32902
  }
32242
32903
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32243
- const receiver = callee.object;
32904
+ const receiver = stripParenExpression(callee.object);
32244
32905
  const property = callee.property;
32245
32906
  if (!isNodeOfType(property, "Identifier")) return null;
32246
32907
  if (isNodeOfType(receiver, "Identifier")) {
@@ -32361,8 +33022,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32361
33022
  if (typeof sourceValue !== "string") return;
32362
33023
  if (handleExtraSource?.(node, context)) return;
32363
33024
  if (sourceValue !== source) return;
33025
+ if (isTypeOnlyImport(node)) return;
32364
33026
  for (const specifier of node.specifiers ?? []) {
32365
33027
  if (isNodeOfType(specifier, "ImportSpecifier")) {
33028
+ if (specifier.importKind === "type") continue;
32366
33029
  const importedName = getImportedName$1(specifier);
32367
33030
  if (!importedName) continue;
32368
33031
  const message = messages.get(importedName);
@@ -32381,8 +33044,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32381
33044
  MemberExpression(node) {
32382
33045
  if (namespaceBindings.size === 0) return;
32383
33046
  if (node.computed) return;
32384
- if (!isNodeOfType(node.object, "Identifier")) return;
32385
- if (!namespaceBindings.has(node.object.name)) return;
33047
+ const receiver = stripParenExpression(node.object);
33048
+ if (!isNodeOfType(receiver, "Identifier")) return;
33049
+ if (!namespaceBindings.has(receiver.name)) return;
32386
33050
  if (!isNodeOfType(node.property, "Identifier")) return;
32387
33051
  const message = messages.get(node.property.name);
32388
33052
  if (message) context.report({
@@ -32447,7 +33111,7 @@ const noReactDomDeprecatedApis = defineRule({
32447
33111
  });
32448
33112
  //#endregion
32449
33113
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
32450
- 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'`."]]);
33114
+ 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."]]);
32451
33115
  const isVendoredShadcnUiFilename = (rawFilename) => {
32452
33116
  if (!rawFilename) return false;
32453
33117
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32485,7 +33149,7 @@ const noReact19DeprecatedApis = defineRule({
32485
33149
  requires: ["react:19"],
32486
33150
  tags: ["test-noise", "migration-hint"],
32487
33151
  severity: "warn",
32488
- 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.",
33152
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32489
33153
  create: (context) => {
32490
33154
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32491
33155
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -32809,34 +33473,11 @@ const noRedundantShouldComponentUpdate = defineRule({
32809
33473
  });
32810
33474
  //#endregion
32811
33475
  //#region src/plugin/rules/architecture/no-render-in-render.ts
32812
- const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32813
- if (!symbol || visitedSymbols.has(symbol)) return false;
32814
- visitedSymbols.add(symbol);
32815
- if (isComponentParameterSymbol(symbol)) return true;
32816
- if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
32817
- const source = symbol.initializer;
32818
- if (!source) return false;
32819
- return initializerRootsInProps(source, scopes, visitedSymbols);
32820
- };
32821
- const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32822
- if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
32823
- if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
32824
- return rootsInProps(node, scopes, visitedSymbols);
32825
- };
32826
- const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32827
- let current = node;
32828
- while (isNodeOfType(current, "MemberExpression")) {
32829
- if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
32830
- current = current.object;
32831
- }
32832
- if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
32833
- return false;
32834
- };
32835
33476
  const isInsideComponentContext = (node) => {
32836
33477
  let cursor = node.parent;
32837
33478
  while (cursor) {
32838
- if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
32839
33479
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33480
+ if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
32840
33481
  cursor = cursor.parent ?? null;
32841
33482
  }
32842
33483
  return false;
@@ -32846,24 +33487,28 @@ const functionBodyOf = (node) => {
32846
33487
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
32847
33488
  return null;
32848
33489
  };
33490
+ const isHookCallee = (callee) => {
33491
+ if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
33492
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
33493
+ return false;
33494
+ };
32849
33495
  const containsHookCall = (body) => {
32850
33496
  let found = false;
32851
33497
  walkAst(body, (child) => {
32852
- if (found) return;
33498
+ if (found) return false;
33499
+ if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32853
33500
  if (!isNodeOfType(child, "CallExpression")) return;
32854
- const name = getCalleeName$2(child);
32855
- if (name && isReactHookName(name)) found = true;
33501
+ if (isHookCallee(child.callee)) found = true;
32856
33502
  });
32857
33503
  return found;
32858
33504
  };
32859
- const isModuleScopeHookFreeHelper = (symbol) => {
33505
+ const isHookCallingRenderHelper = (symbol) => {
32860
33506
  if (!symbol) return false;
32861
33507
  const declaration = symbol.declarationNode;
32862
33508
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
32863
33509
  const body = functionBodyOf(declaration);
32864
33510
  if (!body) return false;
32865
- if (findEnclosingFunction(declaration) !== null) return false;
32866
- return !containsHookCall(body);
33511
+ return containsHookCall(body);
32867
33512
  };
32868
33513
  const noRenderInRender = defineRule({
32869
33514
  id: "no-render-in-render",
@@ -32872,20 +33517,13 @@ const noRenderInRender = defineRule({
32872
33517
  tags: ["test-noise"],
32873
33518
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
32874
33519
  create: (context) => ({ JSXExpressionContainer(node) {
32875
- const expression = node.expression;
33520
+ const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
32876
33521
  if (!isNodeOfType(expression, "CallExpression")) return;
32877
- let calleeName = null;
32878
- if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
32879
- else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
32880
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33522
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
33523
+ const calleeName = expression.callee.name;
33524
+ if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32881
33525
  if (!isInsideComponentContext(node)) return;
32882
- if (isNodeOfType(expression.callee, "Identifier")) {
32883
- const calleeSymbol = context.scopes.symbolFor(expression.callee);
32884
- if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
32885
- if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
32886
- } else if (isNodeOfType(expression.callee, "MemberExpression")) {
32887
- if (rootsInProps(expression.callee.object, context.scopes)) return;
32888
- }
33526
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32889
33527
  context.report({
32890
33528
  node: expression,
32891
33529
  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.`
@@ -32946,13 +33584,7 @@ const noRenderPropChildren = defineRule({
32946
33584
  //#endregion
32947
33585
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
32948
33586
  const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
32949
- const isReactDomRenderCall = (node) => {
32950
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
32951
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
32952
- if (node.callee.object.name !== "ReactDOM") return false;
32953
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
32954
- return node.callee.property.name === "render";
32955
- };
33587
+ const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
32956
33588
  const isUsedAsReturnValue = (parent) => {
32957
33589
  if (!parent) return false;
32958
33590
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -33788,7 +34420,7 @@ const noSetState = defineRule({
33788
34420
  category: "Architecture",
33789
34421
  create: (context) => ({ CallExpression(node) {
33790
34422
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
33791
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
34423
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33792
34424
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
33793
34425
  if (!getParentComponent(node)) return;
33794
34426
  context.report({
@@ -36234,6 +36866,36 @@ const isTriviallyCheapExpression = (node) => {
36234
36866
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36235
36867
  return true;
36236
36868
  };
36869
+ const isTrivialContainerLiteral = (node) => {
36870
+ if (!node) return false;
36871
+ const innerExpression = stripParenExpression(node);
36872
+ if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
36873
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
36874
+ return false;
36875
+ };
36876
+ const isNonEscapingRead = (identifier) => {
36877
+ const readRoot = findTransparentExpressionRoot(identifier);
36878
+ const memberNode = readRoot.parent;
36879
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
36880
+ const memberUse = findTransparentExpressionRoot(memberNode);
36881
+ const memberUseParent = memberUse.parent;
36882
+ if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
36883
+ if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
36884
+ if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
36885
+ return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
36886
+ };
36887
+ const isMemoIdentityUnused = (memoCallNode, scopes) => {
36888
+ const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
36889
+ const parentNode = memoUsageRoot.parent;
36890
+ if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
36891
+ if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
36892
+ const bindingTarget = parentNode.id;
36893
+ if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
36894
+ if (!isNodeOfType(bindingTarget, "Identifier")) return false;
36895
+ const symbol = scopes.symbolFor(bindingTarget);
36896
+ if (!symbol) return false;
36897
+ return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
36898
+ };
36237
36899
  const noUsememoSimpleExpression = defineRule({
36238
36900
  id: "no-usememo-simple-expression",
36239
36901
  title: "useMemo on a cheap value",
@@ -36255,9 +36917,17 @@ const noUsememoSimpleExpression = defineRule({
36255
36917
  let returnExpression = null;
36256
36918
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36257
36919
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36258
- if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
36920
+ if (!returnExpression) return;
36921
+ if (isTriviallyCheapExpression(returnExpression)) {
36922
+ context.report({
36923
+ node,
36924
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36925
+ });
36926
+ return;
36927
+ }
36928
+ if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36259
36929
  node,
36260
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36930
+ message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36261
36931
  });
36262
36932
  } })
36263
36933
  });
@@ -36363,7 +37033,7 @@ const noWillUpdateSetState = defineRule({
36363
37033
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
36364
37034
  return { CallExpression(node) {
36365
37035
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
36366
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
37036
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36367
37037
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36368
37038
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36369
37039
  context.report({
@@ -36719,7 +37389,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
36719
37389
  continue;
36720
37390
  }
36721
37391
  if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
36722
- if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
37392
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
36723
37393
  if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
36724
37394
  }
36725
37395
  return false;
@@ -36827,18 +37497,22 @@ const onlyExportComponents = defineRule({
36827
37497
  customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36828
37498
  allowExportNames: new Set(settings.allowExportNames),
36829
37499
  allowConstantExport: settings.allowConstantExport,
36830
- localComponentNames
37500
+ localComponentNames,
37501
+ scopes: context.scopes
36831
37502
  };
36832
37503
  for (const child of componentCandidates) {
36833
37504
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36834
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37505
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
36835
37506
  }
36836
37507
  if (isNodeOfType(child, "ClassDeclaration") && child.id) {
36837
37508
  if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36838
37509
  }
36839
37510
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36840
37511
  const initializer = child.init;
36841
- if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37512
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
37513
+ const expression = initializer ? skipTsExpression(initializer) : null;
37514
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
37515
+ }
36842
37516
  }
36843
37517
  }
36844
37518
  const exports = [];
@@ -38503,11 +39177,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
38503
39177
  }
38504
39178
  return null;
38505
39179
  };
39180
+ const isTrivialLiteralExpression = (expression) => {
39181
+ if (isNodeOfType(expression, "Literal")) return true;
39182
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
39183
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
39184
+ if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
39185
+ return false;
39186
+ };
38506
39187
  const getSingleSetterCallFromHandler = (handler) => {
38507
39188
  const handlerStatements = getCallbackStatements(handler);
38508
39189
  if (handlerStatements.length !== 1) return null;
38509
39190
  const onlyStatement = handlerStatements[0];
38510
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
39191
+ let expression = onlyStatement;
39192
+ if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
39193
+ if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
39194
+ if (!expression) return null;
39195
+ expression = stripParenExpression(expression);
38511
39196
  if (!isNodeOfType(expression, "CallExpression")) return null;
38512
39197
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
38513
39198
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -38517,6 +39202,106 @@ const getSingleSetterCallFromHandler = (handler) => {
38517
39202
  setterArgument: expression.arguments[0]
38518
39203
  };
38519
39204
  };
39205
+ const isListenerCollectionInitializer = (init) => {
39206
+ if (!init) return false;
39207
+ if (isNodeOfType(init, "ArrayExpression")) return true;
39208
+ return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
39209
+ };
39210
+ const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
39211
+ if (!isFunctionLike$1(functionNode)) return false;
39212
+ const firstParam = functionNode.params?.[0];
39213
+ if (!isNodeOfType(firstParam, "Identifier")) return false;
39214
+ const listenerParamName = firstParam.name;
39215
+ let registersListener = false;
39216
+ walkAst(functionNode.body, (child) => {
39217
+ if (registersListener) return false;
39218
+ if (!isNodeOfType(child, "CallExpression")) return;
39219
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
39220
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
39221
+ if (!listenerCollectionNames.has(child.callee.object.name)) return;
39222
+ if (!isNodeOfType(child.callee.property, "Identifier")) return;
39223
+ if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
39224
+ const registeredArgument = child.arguments?.[0];
39225
+ if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
39226
+ });
39227
+ return registersListener;
39228
+ };
39229
+ const buildModuleScopeStoreIndex = (programRoot) => {
39230
+ const mutableBindingNames = /* @__PURE__ */ new Set();
39231
+ const listenerCollectionNames = /* @__PURE__ */ new Set();
39232
+ const moduleFunctionsByName = /* @__PURE__ */ new Map();
39233
+ if (!isNodeOfType(programRoot, "Program")) return {
39234
+ mutableBindingNames,
39235
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39236
+ };
39237
+ for (const statement of programRoot.body ?? []) {
39238
+ const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
39239
+ if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
39240
+ moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
39241
+ continue;
39242
+ }
39243
+ if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
39244
+ for (const declarator of unwrapped.declarations ?? []) {
39245
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
39246
+ const init = declarator.init ?? null;
39247
+ if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
39248
+ mutableBindingNames.add(declarator.id.name);
39249
+ continue;
39250
+ }
39251
+ if (isListenerCollectionInitializer(init)) {
39252
+ listenerCollectionNames.add(declarator.id.name);
39253
+ continue;
39254
+ }
39255
+ if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
39256
+ }
39257
+ }
39258
+ const subscribeFunctionNames = /* @__PURE__ */ new Set();
39259
+ for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
39260
+ return {
39261
+ mutableBindingNames,
39262
+ subscribeFunctionNames
39263
+ };
39264
+ };
39265
+ const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
39266
+ if (!isNodeOfType(useStateCall, "CallExpression")) return null;
39267
+ let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
39268
+ if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
39269
+ if (!isNodeOfType(initialArgument, "Identifier")) return null;
39270
+ if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
39271
+ const binding = findVariableInitializer(initialArgument, initialArgument.name);
39272
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
39273
+ return initialArgument.name;
39274
+ };
39275
+ const argumentForwardsSetter = (argument, setterName) => {
39276
+ if (!argument) return false;
39277
+ const unwrapped = stripParenExpression(argument);
39278
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
39279
+ if (!isFunctionLike$1(unwrapped)) return false;
39280
+ let callsSetter = false;
39281
+ walkAst(unwrapped.body, (child) => {
39282
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
39283
+ callsSetter = true;
39284
+ return false;
39285
+ }
39286
+ });
39287
+ return callsSetter;
39288
+ };
39289
+ const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
39290
+ let matchedCall = null;
39291
+ walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
39292
+ if (matchedCall) return false;
39293
+ if (!isNodeOfType(child, "CallExpression")) return;
39294
+ if (!isNodeOfType(child.callee, "Identifier")) return;
39295
+ if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
39296
+ const binding = findVariableInitializer(child.callee, child.callee.name);
39297
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
39298
+ for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
39299
+ matchedCall = child;
39300
+ return false;
39301
+ }
39302
+ });
39303
+ return matchedCall;
39304
+ };
38520
39305
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
38521
39306
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
38522
39307
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -38533,6 +39318,16 @@ const preferUseSyncExternalStore = defineRule({
38533
39318
  severity: "warn",
38534
39319
  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.",
38535
39320
  create: (context) => {
39321
+ let cachedStoreIndex = null;
39322
+ const storeIndexFor = (node) => {
39323
+ if (cachedStoreIndex) return cachedStoreIndex;
39324
+ const programRoot = findProgramRoot(node);
39325
+ cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
39326
+ mutableBindingNames: /* @__PURE__ */ new Set(),
39327
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39328
+ };
39329
+ return cachedStoreIndex;
39330
+ };
38536
39331
  const checkComponent = (componentBody) => {
38537
39332
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38538
39333
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -38570,6 +39365,7 @@ const preferUseSyncExternalStore = defineRule({
38570
39365
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
38571
39366
  if (!useStateInitializer) continue;
38572
39367
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
39368
+ if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
38573
39369
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
38574
39370
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
38575
39371
  context.report({
@@ -38577,14 +39373,47 @@ const preferUseSyncExternalStore = defineRule({
38577
39373
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
38578
39374
  });
38579
39375
  }
39376
+ checkModuleStoreShape(componentBody, useStateBindings);
39377
+ };
39378
+ const checkModuleStoreShape = (componentBody, useStateBindings) => {
39379
+ const storeIndex = storeIndexFor(componentBody);
39380
+ if (storeIndex.mutableBindingNames.size === 0) return;
39381
+ if (storeIndex.subscribeFunctionNames.size === 0) return;
39382
+ const snapshotBindings = useStateBindings.map((binding) => ({
39383
+ binding,
39384
+ storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
39385
+ })).filter((candidate) => candidate.storeName !== null);
39386
+ if (snapshotBindings.length === 0) return;
39387
+ const reportedDeclarators = /* @__PURE__ */ new Set();
39388
+ for (const effectCall of findUseEffectsInComponent(componentBody)) {
39389
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
39390
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
39391
+ const depsNode = effectCall.arguments[1];
39392
+ if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
39393
+ if ((depsNode.elements?.length ?? 0) !== 0) continue;
39394
+ const callback = getEffectCallback(effectCall);
39395
+ if (!callback || !isFunctionLike$1(callback)) continue;
39396
+ for (const { binding, storeName } of snapshotBindings) {
39397
+ if (reportedDeclarators.has(binding.declarator)) continue;
39398
+ if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
39399
+ reportedDeclarators.add(binding.declarator);
39400
+ context.report({
39401
+ node: binding.declarator,
39402
+ 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.`
39403
+ });
39404
+ }
39405
+ }
38580
39406
  };
38581
39407
  return {
38582
39408
  FunctionDeclaration(node) {
38583
- if (!node.id?.name || !isUppercaseName(node.id.name)) return;
39409
+ const functionName = node.id?.name;
39410
+ if (!functionName) return;
39411
+ if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38584
39412
  checkComponent(node.body);
38585
39413
  },
38586
39414
  VariableDeclarator(node) {
38587
- if (!isComponentAssignment(node)) return;
39415
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
39416
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
38588
39417
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
38589
39418
  checkComponent(node.init.body);
38590
39419
  }
@@ -39194,7 +40023,7 @@ const queryNoVoidQueryFn = defineRule({
39194
40023
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
39195
40024
  const body = queryFnValue.body;
39196
40025
  if (!isNodeOfType(body, "BlockStatement")) return;
39197
- if ((body.body ?? []).length === 0) context.report({
40026
+ if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39198
40027
  node: queryFnProperty,
39199
40028
  message: "This empty queryFn caches undefined, so the component never gets data."
39200
40029
  });
@@ -39701,17 +40530,6 @@ const renderingHoistJsx = defineRule({
39701
40530
  }
39702
40531
  });
39703
40532
  //#endregion
39704
- //#region src/plugin/utils/find-declarator-for-binding.ts
39705
- const findDeclaratorForBinding = (bindingIdentifier) => {
39706
- let ancestor = bindingIdentifier.parent;
39707
- while (ancestor) {
39708
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
39709
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
39710
- ancestor = ancestor.parent ?? null;
39711
- }
39712
- return null;
39713
- };
39714
- //#endregion
39715
40533
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
39716
40534
  const NONDETERMINISTIC_RENDER_PATTERNS = [
39717
40535
  {
@@ -39720,19 +40538,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
39720
40538
  },
39721
40539
  {
39722
40540
  display: "Date.now()",
39723
- 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"
40541
+ matches: (node) => isGlobalMethodCall(node, "Date", "now")
39724
40542
  },
39725
40543
  {
39726
40544
  display: "Math.random()",
39727
- 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"
40545
+ matches: (node) => isGlobalMethodCall(node, "Math", "random")
39728
40546
  },
39729
40547
  {
39730
40548
  display: "performance.now()",
39731
- 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"
40549
+ matches: (node) => isGlobalMethodCall(node, "performance", "now")
39732
40550
  },
39733
40551
  {
39734
40552
  display: "crypto.randomUUID()",
39735
- 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"
40553
+ matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39736
40554
  }
39737
40555
  ];
39738
40556
  const findOpeningElementOfChild = (jsxNode) => {
@@ -39744,54 +40562,6 @@ const findOpeningElementOfChild = (jsxNode) => {
39744
40562
  }
39745
40563
  return null;
39746
40564
  };
39747
- const executesDuringRender = (functionNode) => {
39748
- const parent = functionNode.parent;
39749
- if (!isNodeOfType(parent, "CallExpression")) return false;
39750
- if (parent.callee === functionNode) return true;
39751
- return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
39752
- };
39753
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
39754
- const referencesClientOnlyFlag = (expression) => {
39755
- const unwrapped = stripParenExpression(expression);
39756
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
39757
- if (isNodeOfType(unwrapped, "MemberExpression")) {
39758
- const property = unwrapped.property;
39759
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
39760
- }
39761
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
39762
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
39763
- return false;
39764
- };
39765
- const isFalsyLiteral = (node) => {
39766
- if (!node) return true;
39767
- if (isNodeOfType(node, "Literal")) return !node.value;
39768
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
39769
- };
39770
- const isFalsyInitialStateBinding = (identifier) => {
39771
- if (!isNodeOfType(identifier, "Identifier")) return false;
39772
- const binding = findVariableInitializer(identifier, identifier.name);
39773
- if (!binding) return false;
39774
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
39775
- if (!declarator?.init) return false;
39776
- const init = stripParenExpression(declarator.init);
39777
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
39778
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
39779
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
39780
- return isFalsyLiteral(init.arguments?.[0]);
39781
- };
39782
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
39783
- const isGatedByFalsyInitialState = (node) => {
39784
- let cursor = node;
39785
- let parent = node.parent;
39786
- while (parent) {
39787
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
39788
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39789
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39790
- cursor = parent;
39791
- parent = parent.parent ?? null;
39792
- }
39793
- return false;
39794
- };
39795
40565
  const isYearOnlyDateRead = (dateNode) => {
39796
40566
  const member = dateNode.parent;
39797
40567
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -39815,23 +40585,6 @@ const isInsideMotionTransitionAttribute = (node) => {
39815
40585
  }
39816
40586
  return false;
39817
40587
  };
39818
- const isInsideClientOnlyGuard = (node) => {
39819
- let cursor = node;
39820
- let parent = node.parent;
39821
- while (parent) {
39822
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
39823
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
39824
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
39825
- cursor = parent;
39826
- parent = parent.parent ?? null;
39827
- }
39828
- return false;
39829
- };
39830
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
39831
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
39832
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
39833
- return false;
39834
- };
39835
40588
  const renderingHydrationMismatchTime = defineRule({
39836
40589
  id: "rendering-hydration-mismatch-time",
39837
40590
  title: "Time or random value in JSX",
@@ -39879,6 +40632,35 @@ const renderingHydrationMismatchTime = defineRule({
39879
40632
  }
39880
40633
  });
39881
40634
  //#endregion
40635
+ //#region src/plugin/utils/contains-locale-environment-read.ts
40636
+ const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
40637
+ "toLocaleString",
40638
+ "toLocaleDateString",
40639
+ "toLocaleTimeString",
40640
+ "getTimezoneOffset"
40641
+ ]);
40642
+ const containsLocaleEnvironmentRead = (expression) => {
40643
+ let readsLocaleEnvironment = false;
40644
+ walkAst(expression, (child) => {
40645
+ if (readsLocaleEnvironment) return false;
40646
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
40647
+ if (child.object.name === "Intl") {
40648
+ readsLocaleEnvironment = true;
40649
+ return false;
40650
+ }
40651
+ if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
40652
+ readsLocaleEnvironment = true;
40653
+ return false;
40654
+ }
40655
+ }
40656
+ 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)) {
40657
+ readsLocaleEnvironment = true;
40658
+ return false;
40659
+ }
40660
+ });
40661
+ return readsLocaleEnvironment;
40662
+ };
40663
+ //#endregion
39882
40664
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
39883
40665
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
39884
40666
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -39944,14 +40726,15 @@ const renderingHydrationNoFlicker = defineRule({
39944
40726
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
39945
40727
  const callback = getEffectCallback(node);
39946
40728
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
39947
- const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
39948
- if (!bodyStatements || bodyStatements.length !== 1) return;
40729
+ const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
40730
+ if (bodyStatements.length !== 1) return;
39949
40731
  const soleStatement = bodyStatements[0];
39950
40732
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
39951
40733
  const expression = soleStatement.expression;
39952
40734
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
39953
40735
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
39954
40736
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
40737
+ if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
39955
40738
  context.report({
39956
40739
  node,
39957
40740
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -40770,6 +41553,9 @@ const rerenderFunctionalSetstate = defineRule({
40770
41553
  } })
40771
41554
  });
40772
41555
  //#endregion
41556
+ //#region src/plugin/utils/is-trivial-built-in-construction.ts
41557
+ const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
41558
+ //#endregion
40773
41559
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
40774
41560
  const rerenderLazyRefInit = defineRule({
40775
41561
  id: "rerender-lazy-ref-init",
@@ -40780,7 +41566,7 @@ const rerenderLazyRefInit = defineRule({
40780
41566
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
40781
41567
  create: (context) => ({ CallExpression(node) {
40782
41568
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
40783
- const initializer = node.arguments[0];
41569
+ const initializer = stripParenExpression(node.arguments[0]);
40784
41570
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
40785
41571
  const isNewCall = isNodeOfType(initializer, "NewExpression");
40786
41572
  if (!isPlainCall && !isNewCall) return;
@@ -40788,7 +41574,7 @@ const rerenderLazyRefInit = defineRule({
40788
41574
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40789
41575
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40790
41576
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40791
- if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41577
+ if (isTrivialBuiltInConstruction(initializer)) return;
40792
41578
  if (isPlainCall && isReactHookName(calleeName)) return;
40793
41579
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40794
41580
  context.report({
@@ -40824,11 +41610,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40824
41610
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40825
41611
  const findEagerInitializerCall = (expression, depth = 0) => {
40826
41612
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
40827
- if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
40828
- if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
40829
- if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
40830
- if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
40831
- if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
41613
+ const innerExpression = stripParenExpression(expression);
41614
+ if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
41615
+ if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
41616
+ if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
41617
+ if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
40832
41618
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
40833
41619
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
40834
41620
  if (spreadCall) return spreadCall;
@@ -40851,7 +41637,7 @@ const rerenderLazyStateInit = defineRule({
40851
41637
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40852
41638
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40853
41639
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40854
- if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41640
+ if (isTrivialBuiltInConstruction(initializer)) return;
40855
41641
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
40856
41642
  if (isReactHookName(calleeName)) return;
40857
41643
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -41518,7 +42304,7 @@ const rnAnimationReactionAsDerived = defineRule({
41518
42304
  const body = reactionFn.body;
41519
42305
  let singleAssignment = null;
41520
42306
  if (isNodeOfType(body, "BlockStatement")) {
41521
- const statements = body.body ?? [];
42307
+ const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41522
42308
  if (statements.length !== 1) return;
41523
42309
  const onlyStatement = statements[0];
41524
42310
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -41592,7 +42378,8 @@ const PROMISE_SETTLE_METHODS = new Set([
41592
42378
  "catch",
41593
42379
  "finally"
41594
42380
  ]);
41595
- const findChainRoot = (node) => {
42381
+ const findChainRoot = (wrappedNode) => {
42382
+ const node = stripParenExpression(wrappedNode);
41596
42383
  if (isNodeOfType(node, "CallExpression")) {
41597
42384
  if (isNodeOfType(node.callee, "Identifier")) return {
41598
42385
  calleeName: node.callee.name,
@@ -42180,20 +42967,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
42180
42967
  };
42181
42968
  const isReactNativeDimensionsCallee = (node, callee) => {
42182
42969
  if (!isNodeOfType(callee, "MemberExpression")) return false;
42183
- if (isNodeOfType(callee.object, "Identifier")) {
42184
- const localName = callee.object.name;
42970
+ const receiver = stripParenExpression(callee.object);
42971
+ if (isNodeOfType(receiver, "Identifier")) {
42972
+ const localName = receiver.name;
42185
42973
  const binding = findVariableInitializer(node, localName);
42186
42974
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
42187
42975
  return localName === "Dimensions";
42188
42976
  }
42189
- if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
42190
- if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
42191
- const rootName = getRootIdentifierName(callee.object.object);
42977
+ if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
42978
+ if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
42979
+ const rootName = getRootIdentifierName(receiver.object);
42192
42980
  if (rootName === null) return false;
42193
42981
  const importBinding = getImportBindingForName(node, rootName);
42194
42982
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
42195
42983
  }
42196
- 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";
42984
+ if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42197
42985
  return false;
42198
42986
  };
42199
42987
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -42637,7 +43425,8 @@ const rnNoLegacyShadowStyles = defineRule({
42637
43425
  },
42638
43426
  CallExpression(node) {
42639
43427
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
42640
- if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
43428
+ const receiver = stripParenExpression(node.callee.object);
43429
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42641
43430
  if (!isMemberProperty(node.callee, "create")) return;
42642
43431
  const stylesArgument = node.arguments?.[0];
42643
43432
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -43487,7 +44276,7 @@ const rnNoSetNativeProps = defineRule({
43487
44276
  const callee = node.callee;
43488
44277
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
43489
44278
  if (!isNodeOfType(callee, "MemberExpression")) return;
43490
- if (!isStaticMemberNamed(callee.object, "current")) return;
44279
+ if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43491
44280
  context.report({
43492
44281
  node,
43493
44282
  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."
@@ -43715,14 +44504,15 @@ const analyzeGestureChain = (expression) => {
43715
44504
  if (!isNodeOfType(callee, "MemberExpression")) return null;
43716
44505
  if (!isNodeOfType(callee.property, "Identifier")) return null;
43717
44506
  const methodName = callee.property.name;
43718
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
44507
+ const receiver = stripParenExpression(callee.object);
44508
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43719
44509
  factoryName: methodName,
43720
44510
  chainMethodNames,
43721
44511
  numberOfTapsArgument
43722
44512
  };
43723
44513
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
43724
44514
  chainMethodNames.push(methodName);
43725
- cursor = callee.object;
44515
+ cursor = receiver;
43726
44516
  }
43727
44517
  return null;
43728
44518
  };
@@ -47388,6 +48178,8 @@ const roleSupportsAriaProps = defineRule({
47388
48178
  const propName = propRawName.toLowerCase();
47389
48179
  if (!propName.startsWith("aria-")) continue;
47390
48180
  if (!ARIA_PROPERTIES.has(propName)) continue;
48181
+ const attributeValue = attributeNode.value;
48182
+ if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
47391
48183
  (ariaAttributes ??= []).push({
47392
48184
  attribute,
47393
48185
  propName
@@ -47766,7 +48558,11 @@ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47766
48558
  const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47767
48559
  return Boolean(symbol && symbol.kind !== "import");
47768
48560
  };
47769
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
48561
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
48562
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
48563
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
48564
+ return false;
48565
+ };
47770
48566
  const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47771
48567
  const initializer = symbol.initializer;
47772
48568
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
@@ -48110,7 +48906,8 @@ const serverAfterNonblocking = defineRule({
48110
48906
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48111
48907
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48112
48908
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48113
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
48909
+ const receiver = stripParenExpression(node.callee.object);
48910
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48114
48911
  if (!objectName) return;
48115
48912
  const methodName = node.callee.property.name;
48116
48913
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -48786,7 +49583,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
48786
49583
  };
48787
49584
  const ascendMemberChain = (referenceIdentifier) => {
48788
49585
  let chainTip = referenceIdentifier;
48789
- while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
49586
+ while (chainTip.parent) {
49587
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
49588
+ chainTip = chainTip.parent;
49589
+ continue;
49590
+ }
49591
+ if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
49592
+ chainTip = chainTip.parent;
49593
+ continue;
49594
+ }
49595
+ break;
49596
+ }
48790
49597
  return chainTip;
48791
49598
  };
48792
49599
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -48811,7 +49618,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
48811
49618
  const callee = callExpression.callee;
48812
49619
  if (isNodeOfType(callee, "MemberExpression")) {
48813
49620
  const methodName = getMemberPropertyName$1(callee);
48814
- return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
49621
+ const calleeReceiver = stripParenExpression(callee.object);
49622
+ return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
48815
49623
  }
48816
49624
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
48817
49625
  const calleeSymbol = scopes.symbolFor(callee);
@@ -49541,7 +50349,7 @@ const walkServerFnChain = (outerNode) => {
49541
50349
  };
49542
50350
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
49543
50351
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
49544
- let currentNode = outerNode.callee.object;
50352
+ let currentNode = stripParenExpression(outerNode.callee.object);
49545
50353
  while (isNodeOfType(currentNode, "CallExpression")) {
49546
50354
  const calleeName = getCalleeName$2(currentNode);
49547
50355
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -49552,7 +50360,7 @@ const walkServerFnChain = (outerNode) => {
49552
50360
  }
49553
50361
  }
49554
50362
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
49555
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
50363
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49556
50364
  else break;
49557
50365
  }
49558
50366
  return result;
@@ -50205,7 +51013,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
50205
51013
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
50206
51014
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
50207
51015
  if (methodName) methodNames.unshift(methodName);
50208
- currentNode = currentNode.callee.object;
51016
+ currentNode = stripParenExpression(currentNode.callee.object);
50209
51017
  }
50210
51018
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
50211
51019
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -53287,6 +54095,18 @@ const reactDoctorRules = [
53287
54095
  category: "Bugs"
53288
54096
  }
53289
54097
  },
54098
+ {
54099
+ key: "react-doctor/no-locale-format-in-render",
54100
+ id: "no-locale-format-in-render",
54101
+ source: "react-doctor",
54102
+ originallyExternal: false,
54103
+ rule: {
54104
+ ...noLocaleFormatInRender,
54105
+ framework: "global",
54106
+ category: "Bugs",
54107
+ requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
54108
+ }
54109
+ },
53290
54110
  {
53291
54111
  key: "react-doctor/no-long-transition-duration",
53292
54112
  id: "no-long-transition-duration",