oxlint-plugin-react-doctor 0.7.2-dev.11e9c87 → 0.7.2-dev.2953b25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +92 -0
  2. package/dist/index.js +1530 -430
  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,102 @@ 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/get-range-start.ts
7721
+ const getRangeStart = (node) => {
7722
+ const rangeStart = node.range?.[0];
7723
+ return typeof rangeStart === "number" ? rangeStart : null;
7724
+ };
7725
+ //#endregion
7726
+ //#region src/plugin/utils/is-result-discarded-call.ts
7727
+ const isResultDiscardedCall = (callExpression) => {
7728
+ let node = callExpression;
7729
+ let parent = node.parent;
7730
+ while (parent) {
7731
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
7732
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
7733
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
7734
+ if (isNodeOfType(parent, "ChainExpression")) {
7735
+ node = parent;
7736
+ parent = node.parent;
7737
+ continue;
7738
+ }
7739
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
7740
+ node = parent;
7741
+ parent = node.parent;
7742
+ continue;
7743
+ }
7744
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
7745
+ node = parent;
7746
+ parent = node.parent;
7747
+ continue;
7748
+ }
7749
+ if (isNodeOfType(parent, "SequenceExpression")) {
7750
+ const expressions = parent.expressions ?? [];
7751
+ if (expressions[expressions.length - 1] !== node) return true;
7752
+ node = parent;
7753
+ parent = node.parent;
7754
+ continue;
7755
+ }
7756
+ return false;
7757
+ }
7758
+ return false;
7759
+ };
7760
+ //#endregion
7651
7761
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7652
7762
  const walkInsideStatementBlocks = (node, visitor) => {
7653
7763
  if (!node || typeof node !== "object") return;
@@ -7799,6 +7909,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7799
7909
  };
7800
7910
  //#endregion
7801
7911
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
7912
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
7913
+ const RESOURCE_NOUN_BY_KIND = {
7914
+ subscribe: "subscription",
7915
+ timer: "timer",
7916
+ socket: "connection"
7917
+ };
7918
+ const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
7919
+ const isSubscribeOrObserveCall = (node) => {
7920
+ if (isSubscribeLikeCallExpression(node)) return true;
7921
+ return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
7922
+ };
7802
7923
  const findSubscribeLikeUsages = (callback) => {
7803
7924
  const usages = [];
7804
7925
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7810,6 +7931,14 @@ const findSubscribeLikeUsages = (callback) => {
7810
7931
  }
7811
7932
  walkAst(callback, (child) => {
7812
7933
  if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
7934
+ if (isSocketConstruction(child)) {
7935
+ usages.push({
7936
+ kind: "socket",
7937
+ node: child,
7938
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
7939
+ });
7940
+ return;
7941
+ }
7813
7942
  if (!isNodeOfType(child, "CallExpression")) return;
7814
7943
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7815
7944
  usages.push({
@@ -7819,7 +7948,7 @@ const findSubscribeLikeUsages = (callback) => {
7819
7948
  });
7820
7949
  return;
7821
7950
  }
7822
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
7951
+ if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
7823
7952
  kind: "subscribe",
7824
7953
  node: child,
7825
7954
  resourceName: child.callee.property.name
@@ -7835,6 +7964,13 @@ const collectCleanupBindings = (effectCallback) => {
7835
7964
  };
7836
7965
  if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
7837
7966
  if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
7967
+ walkAst(effectCallback.body, (child) => {
7968
+ if (!isSubscribeOrObserveCall(child)) return;
7969
+ if (!isNodeOfType(child, "CallExpression")) return;
7970
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
7971
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
7972
+ bindings.subscriptionNames.add(child.callee.object.name);
7973
+ });
7838
7974
  walkInsideStatementBlocks(effectCallback.body, (child) => {
7839
7975
  if (!isNodeOfType(child, "VariableDeclaration")) return;
7840
7976
  for (const declarator of child.declarations ?? []) {
@@ -7842,7 +7978,12 @@ const collectCleanupBindings = (effectCallback) => {
7842
7978
  const bindingName = declarator.id.name;
7843
7979
  bindings.effectScopeVariableNames.add(bindingName);
7844
7980
  const init = declarator.init;
7845
- if (!init || !isNodeOfType(init, "CallExpression")) continue;
7981
+ if (!init) continue;
7982
+ if (isSocketConstruction(init)) {
7983
+ bindings.subscriptionNames.add(bindingName);
7984
+ continue;
7985
+ }
7986
+ if (!isNodeOfType(init, "CallExpression")) continue;
7846
7987
  if (isSubscribeLikeCallExpression(init)) {
7847
7988
  bindings.subscriptionNames.add(bindingName);
7848
7989
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -7868,9 +8009,21 @@ const collectCleanupBindings = (effectCallback) => {
7868
8009
  });
7869
8010
  return bindings;
7870
8011
  };
7871
- const getRangeStart = (node) => {
7872
- const rangeStart = node.range?.[0];
7873
- return typeof rangeStart === "number" ? rangeStart : null;
8012
+ const removeSynchronouslyReleasedUsages = (callback, usages) => {
8013
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8014
+ if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8015
+ const releaseStarts = [];
8016
+ walkInsideStatementBlocks(callback.body, (child) => {
8017
+ if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8018
+ const releaseStart = getRangeStart(child);
8019
+ if (releaseStart !== null) releaseStarts.push(releaseStart);
8020
+ });
8021
+ if (releaseStarts.length === 0) return usages;
8022
+ return usages.filter((usage) => {
8023
+ const usageStart = getRangeStart(usage.node);
8024
+ if (usageStart === null) return true;
8025
+ return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8026
+ });
7874
8027
  };
7875
8028
  const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
7876
8029
  if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
@@ -7894,26 +8047,177 @@ const effectHasCleanupReturn = (callback, usages) => {
7894
8047
  });
7895
8048
  return didFindCleanupReturn;
7896
8049
  };
8050
+ const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8051
+ const isSelfReleasingListenerOptionProperty = (property) => {
8052
+ if (!isNodeOfType(property, "Property")) return false;
8053
+ const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
8054
+ if (keyName === "signal") return true;
8055
+ if (keyName !== "once") return false;
8056
+ return isNodeOfType(property.value, "Literal") && property.value.value === true;
8057
+ };
8058
+ const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
8059
+ const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8060
+ ["addEventListener", new Set(["removeEventListener", "abort"])],
8061
+ ["addListener", new Set([
8062
+ "removeListener",
8063
+ "off",
8064
+ "abort"
8065
+ ])],
8066
+ ["on", new Set([
8067
+ "off",
8068
+ "removeListener",
8069
+ "on"
8070
+ ])],
8071
+ ["subscribe", new Set(["unsubscribe", "unsub"])],
8072
+ ["sub", new Set(["unsub", "unsubscribe"])],
8073
+ ["watch", new Set(["unwatch", "close"])],
8074
+ ["listen", new Set(["unlisten", "close"])],
8075
+ [OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
8076
+ ]);
8077
+ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8078
+ "cleanup",
8079
+ "dispose",
8080
+ "destroy",
8081
+ "teardown"
8082
+ ]);
8083
+ const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8084
+ const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8085
+ const getReleaseVerbName = (node) => {
8086
+ if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8087
+ const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8088
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8089
+ const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8090
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
8091
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
8092
+ return null;
8093
+ };
8094
+ const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8095
+ const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8096
+ let didFindPairedRelease = false;
8097
+ walkAst(body, (child) => {
8098
+ if (didFindPairedRelease) return false;
8099
+ if (child !== body && isFunctionLike$1(child)) return false;
8100
+ const releaseVerbName = getReleaseVerbName(child);
8101
+ if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8102
+ didFindPairedRelease = true;
8103
+ return false;
8104
+ }
8105
+ });
8106
+ return didFindPairedRelease;
8107
+ };
8108
+ const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8109
+ const collectFileReleaseVerbNames = (anyNode) => {
8110
+ let programNode = anyNode;
8111
+ while (programNode.parent) programNode = programNode.parent;
8112
+ const cached = fileReleaseVerbNamesCache.get(programNode);
8113
+ if (cached) return cached;
8114
+ const releaseVerbNames = /* @__PURE__ */ new Set();
8115
+ walkAst(programNode, (child) => {
8116
+ const releaseVerbName = getReleaseVerbName(child);
8117
+ if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
8118
+ });
8119
+ fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8120
+ return releaseVerbNames;
8121
+ };
8122
+ const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8123
+ const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8124
+ const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8125
+ if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8126
+ for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
8127
+ return false;
8128
+ };
8129
+ const findRetainedFunctionLeak = (retainedFunction) => {
8130
+ if (!isFunctionLike$1(retainedFunction)) return null;
8131
+ const body = retainedFunction.body;
8132
+ if (!body) return null;
8133
+ const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8134
+ let leak = null;
8135
+ walkAst(body, (child) => {
8136
+ if (leak !== null) return false;
8137
+ if (isFunctionLike$1(child)) return false;
8138
+ if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8139
+ if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8140
+ leak = {
8141
+ kind: "socket",
8142
+ node: child,
8143
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8144
+ };
8145
+ return false;
8146
+ }
8147
+ if (!isNodeOfType(child, "CallExpression")) return;
8148
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8149
+ leak = {
8150
+ kind: "timer",
8151
+ node: child,
8152
+ resourceName: "setInterval"
8153
+ };
8154
+ return false;
8155
+ }
8156
+ if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8157
+ const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8158
+ if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
8159
+ kind: "subscribe",
8160
+ node: child,
8161
+ resourceName: registrationVerbName
8162
+ };
8163
+ return false;
8164
+ }
8165
+ });
8166
+ return leak;
8167
+ };
8168
+ const isRetainedComponentScopeFunction = (functionNode) => {
8169
+ if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
8170
+ if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
8171
+ if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8172
+ return enclosingComponentOrHookName(functionNode) !== null;
8173
+ };
7897
8174
  const effectNeedsCleanup = defineRule({
7898
8175
  id: "effect-needs-cleanup",
7899
8176
  title: "Effect subscription or timer never cleaned up",
7900
8177
  severity: "error",
7901
8178
  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
- } })
8179
+ recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
8180
+ create: (context) => {
8181
+ const reportRetainedLeak = (retainedFunction) => {
8182
+ const leak = findRetainedFunctionLeak(retainedFunction);
8183
+ if (!leak) return;
8184
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8185
+ context.report({
8186
+ node: leak.node,
8187
+ message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
8188
+ });
8189
+ };
8190
+ return {
8191
+ CallExpression(node) {
8192
+ if (isHookCall$2(node, "useCallback")) {
8193
+ const retainedCallback = getEffectCallback(node);
8194
+ if (retainedCallback) reportRetainedLeak(retainedCallback);
8195
+ return;
8196
+ }
8197
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
8198
+ const callback = getEffectCallback(node);
8199
+ if (!callback) return;
8200
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
8201
+ if (usages.length === 0) return;
8202
+ if (effectHasCleanupReturn(callback, usages)) return;
8203
+ const firstUsage = usages[0];
8204
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
8205
+ context.report({
8206
+ node,
8207
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8208
+ });
8209
+ },
8210
+ FunctionDeclaration(node) {
8211
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8212
+ },
8213
+ ArrowFunctionExpression(node) {
8214
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8215
+ },
8216
+ FunctionExpression(node) {
8217
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8218
+ }
8219
+ };
8220
+ }
7917
8221
  });
7918
8222
  //#endregion
7919
8223
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8472,17 +8776,6 @@ const closureCaptures = (functionNode, scopes) => {
8472
8776
  return computedCaptures;
8473
8777
  };
8474
8778
  //#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
8779
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8487
8780
  const reactHocCalleeName = (callee) => {
8488
8781
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -8696,8 +8989,8 @@ const isAstDescendant = (inner, outer) => {
8696
8989
  * One cohesive concept: "given a captured symbol, is its value
8697
8990
  * structurally stable across re-renders (and therefore unnecessary
8698
8991
  * in a deps array)?". The rule reads `symbolHasStableValue` /
8699
- * `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
8700
- * `isRecursiveInitializerCapture` at multiple sites — extracting
8992
+ * `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
8993
+ * multiple sites — extracting
8701
8994
  * them lets the rule body stay focused on the diff-the-captured-vs-
8702
8995
  * declared logic.
8703
8996
  *
@@ -8753,11 +9046,6 @@ const symbolHasStableHookOrigin = (symbol) => {
8753
9046
  }
8754
9047
  return false;
8755
9048
  };
8756
- const symbolHasUseEffectEventOrigin = (symbol) => {
8757
- const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
8758
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
8759
- return getHookName(initializer.callee) === "useEffectEvent";
8760
- };
8761
9049
  const getFunctionValueNode = (symbol) => {
8762
9050
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
8763
9051
  const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
@@ -8843,6 +9131,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
8843
9131
  };
8844
9132
  const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
8845
9133
  //#endregion
9134
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
9135
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
9136
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
9137
+ if (importSource === null) return false;
9138
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
9139
+ };
9140
+ //#endregion
9141
+ //#region src/plugin/utils/is-non-react-effect-event-callee.ts
9142
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
9143
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
9144
+ return Boolean(symbol && symbol.kind !== "import");
9145
+ };
9146
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
9147
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
9148
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
9149
+ return false;
9150
+ };
9151
+ //#endregion
9152
+ //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9153
+ const getUseEffectEventCalleeName = (callee) => {
9154
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
9155
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9156
+ return null;
9157
+ };
9158
+ const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
9159
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
9160
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9161
+ if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
9162
+ return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9163
+ };
9164
+ //#endregion
8846
9165
  //#region src/plugin/rules/react-builtins/exhaustive-deps.ts
8847
9166
  const HOOKS_REQUIRING_DEPS_MATCH = new Set([
8848
9167
  "useEffect",
@@ -9493,7 +9812,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9493
9812
  if (isLiteralOrEmptyTemplate(stripped)) continue;
9494
9813
  if (isNodeOfType(stripped, "Identifier")) {
9495
9814
  const depSymbol = context.scopes.symbolFor(stripped);
9496
- if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
9815
+ if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
9497
9816
  context.report({
9498
9817
  node: elementNode,
9499
9818
  message: buildEffectEventDepMessage()
@@ -9919,7 +10238,10 @@ const PRAGMA = "React";
9919
10238
  const isReactFunctionCall = (node, expectedCall) => {
9920
10239
  if (!isNodeOfType(node, "CallExpression")) return false;
9921
10240
  if (getCalleeName$2(node) !== expectedCall) return false;
9922
- if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10241
+ if (isNodeOfType(node.callee, "MemberExpression")) {
10242
+ const receiver = stripParenExpression(node.callee.object);
10243
+ return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10244
+ }
9923
10245
  return true;
9924
10246
  };
9925
10247
  //#endregion
@@ -11297,16 +11619,6 @@ const collectHandlerReferencedNames = (root) => {
11297
11619
  return names;
11298
11620
  };
11299
11621
  //#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
11622
  //#region src/plugin/utils/get-function-binding-name.ts
11311
11623
  const getFunctionBindingIdentifier = (functionNode) => {
11312
11624
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -12006,14 +12318,15 @@ const jsCacheStorage = defineRule({
12006
12318
  "ArrowFunctionExpression:exit": exitFunctionScope,
12007
12319
  CallExpression(node) {
12008
12320
  if (!isMemberProperty(node.callee, "getItem")) return;
12009
- if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
12321
+ const receiver = stripParenExpression(node.callee.object);
12322
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
12010
12323
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
12011
12324
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
12012
12325
  const storageKey = String(node.arguments[0].value);
12013
12326
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
12014
12327
  storageReadCounts.set(storageKey, readCount);
12015
12328
  if (readCount === 2) {
12016
- const storageName = node.callee.object.name;
12329
+ const storageName = receiver.name;
12017
12330
  context.report({
12018
12331
  node,
12019
12332
  message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
@@ -12027,13 +12340,16 @@ const jsCacheStorage = defineRule({
12027
12340
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
12028
12341
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
12029
12342
  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;
12343
+ if (isNodeOfType(callee, "MemberExpression")) {
12344
+ const receiver = stripParenExpression(callee.object);
12345
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12346
+ if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12347
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12348
+ return true;
12349
+ }
12350
+ return false;
12036
12351
  }
12352
+ if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12037
12353
  return false;
12038
12354
  };
12039
12355
  const isChainPassThroughCall = (callExpression) => {
@@ -12045,10 +12361,7 @@ const isChainPassThroughCall = (callExpression) => {
12045
12361
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12046
12362
  let cursor = receiverNode;
12047
12363
  while (cursor) {
12048
- if (isNodeOfType(cursor, "ChainExpression")) {
12049
- cursor = cursor.expression;
12050
- continue;
12051
- }
12364
+ cursor = stripParenExpression(cursor);
12052
12365
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12053
12366
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12054
12367
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12124,10 +12437,7 @@ const isStringSplitRootedChain = (receiverNode) => {
12124
12437
  let hops = 0;
12125
12438
  while (cursor && hops < 12) {
12126
12439
  hops += 1;
12127
- if (isNodeOfType(cursor, "ChainExpression")) {
12128
- cursor = cursor.expression;
12129
- continue;
12130
- }
12440
+ cursor = stripParenExpression(cursor);
12131
12441
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12132
12442
  const callee = cursor.callee;
12133
12443
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12151,10 +12461,7 @@ const isSmallLiteralArray = (node) => {
12151
12461
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12152
12462
  let cursor = receiverNode;
12153
12463
  while (cursor) {
12154
- if (isNodeOfType(cursor, "ChainExpression")) {
12155
- cursor = cursor.expression;
12156
- continue;
12157
- }
12464
+ cursor = stripParenExpression(cursor);
12158
12465
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12159
12466
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12160
12467
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12219,7 +12526,7 @@ const jsCombineIterations = defineRule({
12219
12526
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12220
12527
  const outerMethod = node.callee.property.name;
12221
12528
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12222
- const innerCall = node.callee.object;
12529
+ const innerCall = stripParenExpression(node.callee.object);
12223
12530
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12224
12531
  const innerMethod = innerCall.callee.property.name;
12225
12532
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12292,10 +12599,10 @@ const jsFlatmapFilter = defineRule({
12292
12599
  if (!filterArgument) return;
12293
12600
  const isIdentityArrow = isNodeOfType(filterArgument, "ArrowFunctionExpression") && filterArgument.params?.length === 1 && isNodeOfType(filterArgument.body, "Identifier") && isNodeOfType(filterArgument.params[0], "Identifier") && filterArgument.body.name === filterArgument.params[0].name;
12294
12601
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12295
- const innerCall = node.callee.object;
12602
+ const innerCall = stripParenExpression(node.callee.object);
12296
12603
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12297
12604
  if (innerCall.callee.property.name !== "map") return;
12298
- const receiver = innerCall.callee.object;
12605
+ const receiver = stripParenExpression(innerCall.callee.object);
12299
12606
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12300
12607
  const elements = receiver.elements ?? [];
12301
12608
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13555,7 +13862,7 @@ const jsTosortedImmutable = defineRule({
13555
13862
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13556
13863
  create: (context) => ({ CallExpression(node) {
13557
13864
  if (!isMemberProperty(node.callee, "sort")) return;
13558
- const receiver = node.callee.object;
13865
+ const receiver = stripParenExpression(node.callee.object);
13559
13866
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13560
13867
  const spreadArgument = receiver.elements[0].argument;
13561
13868
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18592,7 +18899,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18592
18899
  };
18593
18900
  const describeClientSideNavigation = (node) => {
18594
18901
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18595
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
18902
+ const receiver = stripParenExpression(node.callee.object);
18903
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18596
18904
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18597
18905
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18598
18906
  }
@@ -19212,7 +19520,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19212
19520
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19213
19521
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19214
19522
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19215
- if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
19523
+ if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19216
19524
  return node.callee.property.name;
19217
19525
  };
19218
19526
  const isMutatingFetchCall = (node) => {
@@ -19226,7 +19534,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19226
19534
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19227
19535
  const { property, object } = node.callee;
19228
19536
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19229
- if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
19537
+ if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19230
19538
  return true;
19231
19539
  };
19232
19540
  const getDbCallDescription = (node) => {
@@ -19234,7 +19542,8 @@ const getDbCallDescription = (node) => {
19234
19542
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19235
19543
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19236
19544
  const methodName = node.callee.property.name;
19237
- const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19545
+ const receiver = stripParenExpression(node.callee.object);
19546
+ const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19238
19547
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19239
19548
  };
19240
19549
  const findSideEffect = (node, options = {}) => {
@@ -20634,13 +20943,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20634
20943
  if (isNodeOfType(current, "CallExpression")) {
20635
20944
  let node = ref.identifier;
20636
20945
  let parent = node.parent;
20637
- while (parent && isNodeOfType(parent, "MemberExpression")) {
20946
+ while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20638
20947
  node = parent;
20639
20948
  parent = node.parent;
20640
20949
  }
20641
20950
  if (current.callee === node) return current;
20642
20951
  }
20643
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
20952
+ if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20644
20953
  return null;
20645
20954
  };
20646
20955
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -20775,7 +21084,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
20775
21084
  const importDeclaration = declarationNode.parent;
20776
21085
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
20777
21086
  }));
20778
- const isHookCallee = (analysis, node, hookName) => {
21087
+ const isHookCallee$1 = (analysis, node, hookName) => {
20779
21088
  if (!node) return false;
20780
21089
  if (isNodeOfType(node, "Identifier")) {
20781
21090
  if (node.name === hookName) return true;
@@ -20784,15 +21093,19 @@ const isHookCallee = (analysis, node, hookName) => {
20784
21093
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
20785
21094
  return false;
20786
21095
  }
20787
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21096
+ if (isNodeOfType(node, "MemberExpression")) {
21097
+ const receiver = stripParenExpression(node.object);
21098
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21099
+ }
20788
21100
  return false;
20789
21101
  };
20790
21102
  const isUseEffect = (node) => {
20791
21103
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
20792
21104
  const callee = node.callee;
20793
21105
  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;
21106
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
21107
+ const receiver = stripParenExpression(callee.object);
21108
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
20796
21109
  };
20797
21110
  const getEffectFn = (analysis, node) => {
20798
21111
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -20820,7 +21133,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20820
21133
  const node = def.node;
20821
21134
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20822
21135
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20823
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21136
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20824
21137
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20825
21138
  const elements = node.id.elements ?? [];
20826
21139
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -20831,7 +21144,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
20831
21144
  const node = def.node;
20832
21145
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20833
21146
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20834
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21147
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20835
21148
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20836
21149
  const elements = node.id.elements ?? [];
20837
21150
  if (elements.length !== 2) return false;
@@ -20878,7 +21191,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20878
21191
  const node = def.node;
20879
21192
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20880
21193
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20881
- return isHookCallee(analysis, node.init.callee, "useRef");
21194
+ return isHookCallee$1(analysis, node.init.callee, "useRef");
20882
21195
  }));
20883
21196
  const isRefCurrent = (ref) => {
20884
21197
  const parent = ref.identifier.parent;
@@ -20891,11 +21204,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
20891
21204
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
20892
21205
  const isPropCallbackInvocationRef = (analysis, ref) => {
20893
21206
  if (!isPropAlias(analysis, ref)) return false;
20894
- const identifier = ref.identifier;
20895
- const parent = identifier.parent;
21207
+ let effectiveNode = ref.identifier;
21208
+ let parent = effectiveNode.parent;
21209
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21210
+ effectiveNode = parent;
21211
+ parent = effectiveNode.parent;
21212
+ }
20896
21213
  if (!parent) return false;
20897
- if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
20898
- if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21214
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21215
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
20899
21216
  const memberParent = parent.parent;
20900
21217
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
20901
21218
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -20906,7 +21223,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
20906
21223
  };
20907
21224
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
20908
21225
  const getUseStateDecl = (analysis, ref) => {
20909
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21226
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
20910
21227
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
20911
21228
  return node ?? null;
20912
21229
  };
@@ -21111,7 +21428,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21111
21428
  const noAdjustStateOnPropChange = defineRule({
21112
21429
  id: "no-adjust-state-on-prop-change",
21113
21430
  title: "State synced to a prop inside an effect",
21114
- severity: "error",
21431
+ severity: "warn",
21115
21432
  tags: ["test-noise"],
21116
21433
  recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
21117
21434
  create: (context) => ({ CallExpression(node) {
@@ -21152,7 +21469,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21152
21469
  "summary",
21153
21470
  "textarea"
21154
21471
  ]);
21472
+ const isStaticallyFalseBooleanAttribute = (attribute) => {
21473
+ const value = attribute.value;
21474
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
21475
+ const expression = value.expression;
21476
+ return isNodeOfType(expression, "Literal") && expression.value === false;
21477
+ };
21478
+ const DISABLEABLE_TAGS = new Set([
21479
+ "button",
21480
+ "input",
21481
+ "select",
21482
+ "textarea"
21483
+ ]);
21155
21484
  const isNativelyFocusable = (tagName, openingElement) => {
21485
+ if (DISABLEABLE_TAGS.has(tagName)) {
21486
+ const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
21487
+ if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
21488
+ }
21156
21489
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21157
21490
  switch (tagName) {
21158
21491
  case "input": {
@@ -21166,7 +21499,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
21166
21499
  case "a":
21167
21500
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21168
21501
  case "audio":
21169
- case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
21502
+ case "video": {
21503
+ const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
21504
+ return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
21505
+ }
21170
21506
  default: return false;
21171
21507
  }
21172
21508
  };
@@ -22130,7 +22466,8 @@ const SECOND_INDEX_METHODS = new Set([
22130
22466
  "some"
22131
22467
  ]);
22132
22468
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22133
- const isPositionallyStableIterationReceiver = (receiver) => {
22469
+ const isPositionallyStableIterationReceiver = (receiverNode) => {
22470
+ const receiver = stripParenExpression(receiverNode);
22134
22471
  if (isAllLiteralArrayExpression(receiver)) return true;
22135
22472
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22136
22473
  const only = receiver.elements[0];
@@ -22141,17 +22478,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
22141
22478
  }
22142
22479
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22143
22480
  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;
22481
+ if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22145
22482
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22146
22483
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22147
22484
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22148
22485
  return false;
22149
22486
  };
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
- };
22487
+ const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22155
22488
  const isArrayFromSourcePositionallyStable = (source) => {
22156
22489
  if (isNodeOfType(source, "ObjectExpression")) {
22157
22490
  for (const property of source.properties ?? []) {
@@ -22210,18 +22543,12 @@ const expressionUsesIndex = (expression, paramName) => {
22210
22543
  return false;
22211
22544
  }
22212
22545
  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;
22546
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22214
22547
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22215
22548
  }
22216
22549
  return false;
22217
22550
  };
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
- };
22551
+ const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22225
22552
  const noArrayIndexKey = defineRule({
22226
22553
  id: "no-array-index-key",
22227
22554
  title: "Array index used as a key",
@@ -22945,7 +23272,7 @@ const isAsyncFunctionLike = (node) => {
22945
23272
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
22946
23273
  return false;
22947
23274
  };
22948
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23275
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
22949
23276
  "forEach",
22950
23277
  "map",
22951
23278
  "filter",
@@ -22966,30 +23293,51 @@ const runsOnEffectDispatch = (functionNode) => {
22966
23293
  if (parent.callee === functionNode) return true;
22967
23294
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
22968
23295
  const callee = parent.callee;
22969
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
23296
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
22970
23297
  };
22971
23298
  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) => {
23299
+ const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
23300
+ const analyzeStatementSequence = (statements, context) => {
22981
23301
  let fallThroughCount = 0;
22982
- let maxTerminatingPathCount = 0;
23302
+ let maxTerminatedCount = 0;
22983
23303
  for (const statement of statements) {
22984
- const guardBranch = isGuardWithTerminatingBranch(statement);
22985
- if (guardBranch) {
22986
- maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
23304
+ if (isNodeOfType(statement, "IfStatement")) {
23305
+ fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
23306
+ const thenSummary = analyzeBranchStatements(statement.consequent, context);
23307
+ const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
23308
+ fallThroughCount: 0,
23309
+ maxTerminatedCount: 0,
23310
+ doAllPathsTerminate: false
23311
+ };
23312
+ maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
23313
+ if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
23314
+ fallThroughCount: 0,
23315
+ maxTerminatedCount,
23316
+ doAllPathsTerminate: true
23317
+ };
23318
+ const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
23319
+ fallThroughCount += Math.max(...fallThroughBranchCounts);
22987
23320
  continue;
22988
23321
  }
22989
- if (isTerminatingStatement(statement)) break;
23322
+ if (isTerminatingStatement(statement)) {
23323
+ const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
23324
+ return {
23325
+ fallThroughCount: 0,
23326
+ maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
23327
+ doAllPathsTerminate: true
23328
+ };
23329
+ }
22990
23330
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
22991
23331
  }
22992
- return Math.max(maxTerminatingPathCount, fallThroughCount);
23332
+ return {
23333
+ fallThroughCount,
23334
+ maxTerminatedCount,
23335
+ doAllPathsTerminate: false
23336
+ };
23337
+ };
23338
+ const countStatementSequenceSetStateCalls = (statements, context) => {
23339
+ const summary = analyzeStatementSequence(statements, context);
23340
+ return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
22993
23341
  };
22994
23342
  const collectLocalHelperFunctions = (root) => {
22995
23343
  const helpersByName = /* @__PURE__ */ new Map();
@@ -23120,7 +23468,9 @@ const isDevOnlyGuardedEffect = (callback) => {
23120
23468
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23121
23469
  const firstStatement = (body.body ?? [])[0];
23122
23470
  if (!firstStatement) return false;
23123
- if (!isGuardWithTerminatingBranch(firstStatement)) return false;
23471
+ if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
23472
+ const consequent = firstStatement.consequent;
23473
+ if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23124
23474
  return mentionsDevEnvFlag(firstStatement.test);
23125
23475
  };
23126
23476
  const noCascadingSetState = defineRule({
@@ -23479,40 +23829,6 @@ const noCloneElement = defineRule({
23479
23829
  } })
23480
23830
  });
23481
23831
  //#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
23832
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23517
23833
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23518
23834
  const CONTEXT_MODULES = [
@@ -24483,11 +24799,21 @@ const isInitialOnlySetterCall = (callExpr) => {
24483
24799
  return isInitialOnlyPropName(arg.name);
24484
24800
  };
24485
24801
  //#endregion
24802
+ //#region src/plugin/utils/is-no-op-statement.ts
24803
+ const isNoOpStatement = (statement) => {
24804
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
24805
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24806
+ const expression = stripParenExpression(statement.expression);
24807
+ if (isNodeOfType(expression, "Literal")) return true;
24808
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
24809
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
24810
+ return false;
24811
+ };
24812
+ //#endregion
24486
24813
  //#region src/plugin/utils/get-callback-statements.ts
24487
24814
  const getCallbackStatements = (callback) => {
24488
24815
  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] : [];
24816
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24491
24817
  };
24492
24818
  //#endregion
24493
24819
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24526,6 +24852,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24526
24852
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24527
24853
  }
24528
24854
  };
24855
+ const flattenGuardedStatements = (statements) => {
24856
+ const flattened = [];
24857
+ for (const statement of statements) {
24858
+ if (isNoOpStatement(statement)) continue;
24859
+ if (isNodeOfType(statement, "ExpressionStatement")) {
24860
+ flattened.push(statement);
24861
+ continue;
24862
+ }
24863
+ if (isNodeOfType(statement, "IfStatement")) {
24864
+ for (const branch of [statement.consequent, statement.alternate]) {
24865
+ if (!branch) continue;
24866
+ const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
24867
+ if (flattenedBranch === null) return null;
24868
+ flattened.push(...flattenedBranch);
24869
+ }
24870
+ continue;
24871
+ }
24872
+ return null;
24873
+ }
24874
+ return flattened;
24875
+ };
24529
24876
  const noDerivedStateEffect = defineRule({
24530
24877
  id: "no-derived-state-effect",
24531
24878
  title: "Derived state stored in an effect",
@@ -24557,8 +24904,8 @@ const noDerivedStateEffect = defineRule({
24557
24904
  }
24558
24905
  }
24559
24906
  if (sawAnyDep && allDepsAreInitialOnly) return;
24560
- const statements = getCallbackStatements(callback);
24561
- if (statements.length === 0) return;
24907
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
24908
+ if (statements === null || statements.length === 0) return;
24562
24909
  if (!statements.every((statement) => {
24563
24910
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24564
24911
  const expression = statement.expression;
@@ -25193,7 +25540,7 @@ const noDidMountSetState = defineRule({
25193
25540
  const { mode } = resolveSettings$20(context.settings);
25194
25541
  return { CallExpression(node) {
25195
25542
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25196
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25543
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25197
25544
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25198
25545
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25199
25546
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25318,7 +25665,7 @@ const noDidUpdateSetState = defineRule({
25318
25665
  const { mode } = resolveSettings$19(context.settings);
25319
25666
  return { CallExpression(node) {
25320
25667
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25321
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25668
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25322
25669
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25323
25670
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25324
25671
  if (isInsideDiffGuard(node)) return;
@@ -25415,18 +25762,45 @@ const collectUseStateBindings = (componentBody) => {
25415
25762
  //#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
25416
25763
  const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
25417
25764
  const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
25765
+ const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
25766
+ const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
25767
+ "assign",
25768
+ "entries",
25769
+ "fromEntries",
25770
+ "keys",
25771
+ "values"
25772
+ ]);
25773
+ const ARRAY_COPY_METHOD_NAMES = new Set([
25774
+ "map",
25775
+ "filter",
25776
+ "slice",
25777
+ "concat",
25778
+ "flat",
25779
+ "flatMap",
25780
+ "toSorted",
25781
+ "toReversed",
25782
+ "toSpliced",
25783
+ "with"
25784
+ ]);
25418
25785
  const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
25419
25786
  const isPlainDataProducerCall = (expression) => {
25420
25787
  if (!isNodeOfType(expression, "CallExpression")) return false;
25421
25788
  const callee = expression.callee;
25422
25789
  if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
25423
25790
  if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
25424
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25791
+ if (isNodeOfType(callee.object, "Identifier")) {
25792
+ if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25793
+ if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
25794
+ if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
25795
+ }
25425
25796
  return producesPlainStateValue(callee.object);
25426
25797
  };
25798
+ const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
25799
+ const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
25427
25800
  const producesPlainStateValue = (expression) => {
25428
25801
  const unwrapped = stripParenExpression(expression);
25429
25802
  if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
25803
+ if (isPlainDataNewExpression(unwrapped)) return true;
25430
25804
  if (isNullOrUndefinedExpression(unwrapped)) return true;
25431
25805
  if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
25432
25806
  return isPlainDataProducerCall(unwrapped);
@@ -25441,6 +25815,38 @@ const initializerMarksPlainState = (initializerArgument) => {
25441
25815
  }
25442
25816
  return producesPlainStateValue(unwrapped);
25443
25817
  };
25818
+ const producesOpaqueInstanceValue = (expression) => {
25819
+ if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
25820
+ if (!isNodeOfType(expression, "CallExpression")) return false;
25821
+ const callee = expression.callee;
25822
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
25823
+ if (isPlainDataProducerCall(expression)) return false;
25824
+ if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
25825
+ return true;
25826
+ };
25827
+ const collectSetterValueObservations = (componentBody, setterNames) => {
25828
+ const plainFedSetterNames = /* @__PURE__ */ new Set();
25829
+ const opaqueFedSetterNames = /* @__PURE__ */ new Set();
25830
+ walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
25831
+ if (!isNodeOfType(node, "CallExpression")) return;
25832
+ if (!isNodeOfType(node.callee, "Identifier")) return;
25833
+ const setterName = node.callee.name;
25834
+ if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
25835
+ const argument = node.arguments?.[0];
25836
+ if (!argument) return;
25837
+ const unwrapped = stripParenExpression(argument);
25838
+ if (isNullOrUndefinedExpression(unwrapped)) return;
25839
+ if (producesPlainStateValue(unwrapped)) {
25840
+ plainFedSetterNames.add(setterName);
25841
+ return;
25842
+ }
25843
+ if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
25844
+ }, true);
25845
+ return {
25846
+ plainFedSetterNames,
25847
+ opaqueFedSetterNames
25848
+ };
25849
+ };
25444
25850
  const collectCallbackRefSetterNames = (componentBody) => {
25445
25851
  const callbackRefSetterNames = /* @__PURE__ */ new Set();
25446
25852
  walkAst(componentBody, (node) => {
@@ -25510,11 +25916,15 @@ const noDirectStateMutation = defineRule({
25510
25916
  if (bindings.length === 0) return;
25511
25917
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
25512
25918
  const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
25919
+ const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
25513
25920
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
25514
25921
  for (const binding of bindings) {
25515
25922
  if (callbackRefSetterNames.has(binding.setterName)) continue;
25516
25923
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
25517
- if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
25924
+ const initializerArgument = binding.declarator.init.arguments?.[0];
25925
+ if (!initializerMarksPlainState(initializerArgument)) continue;
25926
+ if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
25927
+ plainObjectStateValueNames.add(binding.valueName);
25518
25928
  }
25519
25929
  const visitMutationCandidate = (child, currentlyShadowed) => {
25520
25930
  if (isNodeOfType(child, "AssignmentExpression")) {
@@ -25639,9 +26049,10 @@ const noDocumentStartViewTransition = defineRule({
25639
26049
  create: (context) => ({ CallExpression(node) {
25640
26050
  const callee = node.callee;
25641
26051
  if (!isNodeOfType(callee, "MemberExpression")) return;
25642
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26052
+ const receiver = stripParenExpression(callee.object);
26053
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25643
26054
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
25644
- if (context.scopes.symbolFor(callee.object) !== null) return;
26055
+ if (context.scopes.symbolFor(receiver) !== null) return;
25645
26056
  if (!importsReactViewTransition(node)) return;
25646
26057
  context.report({
25647
26058
  node,
@@ -25661,7 +26072,8 @@ const noDocumentWrite = defineRule({
25661
26072
  create: (context) => ({ CallExpression(node) {
25662
26073
  const callee = node.callee;
25663
26074
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
25664
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26075
+ const receiver = stripParenExpression(callee.object);
26076
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25665
26077
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
25666
26078
  context.report({
25667
26079
  node,
@@ -26293,13 +26705,6 @@ const noEffectEventHandler = defineRule({
26293
26705
  }
26294
26706
  });
26295
26707
  //#endregion
26296
- //#region src/plugin/utils/is-imported-from-non-react-module.ts
26297
- const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
26298
- const importSource = getImportSourceForName(contextNode, localIdentifierName);
26299
- if (importSource === null) return false;
26300
- return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
26301
- };
26302
- //#endregion
26303
26708
  //#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
26304
26709
  const createComponentBindingStackTracker = (callbacks) => {
26305
26710
  const componentBindingStack = [];
@@ -26353,11 +26758,7 @@ const noEffectEventInDeps = defineRule({
26353
26758
  const initializer = declaratorNode.init;
26354
26759
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26355
26760
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26356
- if (isNodeOfType(initializer.callee, "Identifier")) {
26357
- if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26358
- const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26359
- if (calleeSymbol && calleeSymbol.kind !== "import") return;
26360
- }
26761
+ if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
26361
26762
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26362
26763
  } });
26363
26764
  return {
@@ -28937,7 +29338,7 @@ const noIsMounted = defineRule({
28937
29338
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
28938
29339
  create: (context) => ({ CallExpression(node) {
28939
29340
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
28940
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
29341
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
28941
29342
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
28942
29343
  if (!getParentComponent(node)) return;
28943
29344
  context.report({
@@ -28952,7 +29353,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
28952
29353
  const isJsonMethodCall = (node, method) => {
28953
29354
  if (!isNodeOfType(node, "CallExpression")) return false;
28954
29355
  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;
29356
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
29357
+ const receiver = stripParenExpression(callee.object);
29358
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
28956
29359
  };
28957
29360
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
28958
29361
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -29037,7 +29440,7 @@ const extractReturnTypeAnnotation = (returnType) => {
29037
29440
  const noJsxElementType = defineRule({
29038
29441
  id: "no-jsx-element-type",
29039
29442
  title: "No JSX.Element",
29040
- severity: "error",
29443
+ severity: "warn",
29041
29444
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
29042
29445
  create: (context) => {
29043
29446
  let isJsxImported = false;
@@ -29363,6 +29766,378 @@ const noLegacyContextApi = defineRule({
29363
29766
  }
29364
29767
  });
29365
29768
  //#endregion
29769
+ //#region src/plugin/utils/executes-during-render.ts
29770
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29771
+ "map",
29772
+ "filter",
29773
+ "forEach",
29774
+ "flatMap",
29775
+ "reduce",
29776
+ "reduceRight",
29777
+ "some",
29778
+ "every",
29779
+ "find",
29780
+ "findIndex",
29781
+ "findLast",
29782
+ "findLastIndex",
29783
+ "sort",
29784
+ "toSorted"
29785
+ ]);
29786
+ const executesDuringRender = (functionNode) => {
29787
+ const parent = functionNode.parent;
29788
+ if (!isNodeOfType(parent, "CallExpression")) return false;
29789
+ if (parent.callee === functionNode) return true;
29790
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
29791
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
29792
+ };
29793
+ //#endregion
29794
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29795
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
29796
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29797
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29798
+ return false;
29799
+ };
29800
+ //#endregion
29801
+ //#region src/plugin/utils/find-declarator-for-binding.ts
29802
+ const findDeclaratorForBinding = (bindingIdentifier) => {
29803
+ let ancestor = bindingIdentifier.parent;
29804
+ while (ancestor) {
29805
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29806
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29807
+ ancestor = ancestor.parent ?? null;
29808
+ }
29809
+ return null;
29810
+ };
29811
+ //#endregion
29812
+ //#region src/plugin/utils/references-falsy-initial-state.ts
29813
+ const isFalsyLiteral = (node) => {
29814
+ if (!node) return true;
29815
+ if (isNodeOfType(node, "Literal")) return !node.value;
29816
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
29817
+ };
29818
+ const isFalsyInitialStateBinding = (identifier) => {
29819
+ if (!isNodeOfType(identifier, "Identifier")) return false;
29820
+ const binding = findVariableInitializer(identifier, identifier.name);
29821
+ if (!binding) return false;
29822
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29823
+ if (!declarator?.init) return false;
29824
+ const init = stripParenExpression(declarator.init);
29825
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
29826
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
29827
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
29828
+ return isFalsyLiteral(init.arguments?.[0]);
29829
+ };
29830
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
29831
+ //#endregion
29832
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29833
+ const isGatedByFalsyInitialState = (node) => {
29834
+ let cursor = node;
29835
+ let parent = node.parent;
29836
+ while (parent) {
29837
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
29838
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29839
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29840
+ cursor = parent;
29841
+ parent = parent.parent ?? null;
29842
+ }
29843
+ return false;
29844
+ };
29845
+ //#endregion
29846
+ //#region src/plugin/utils/references-client-only-flag.ts
29847
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
29848
+ const referencesClientOnlyFlag = (expression) => {
29849
+ const unwrapped = stripParenExpression(expression);
29850
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
29851
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
29852
+ const property = unwrapped.property;
29853
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
29854
+ }
29855
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
29856
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
29857
+ return false;
29858
+ };
29859
+ //#endregion
29860
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
29861
+ const isInsideClientOnlyGuard = (node) => {
29862
+ let cursor = node;
29863
+ let parent = node.parent;
29864
+ while (parent) {
29865
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
29866
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
29867
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
29868
+ cursor = parent;
29869
+ parent = parent.parent ?? null;
29870
+ }
29871
+ return false;
29872
+ };
29873
+ //#endregion
29874
+ //#region src/plugin/rules/performance/no-locale-format-in-render.ts
29875
+ const LOCALE_FORMAT_METHOD_NAMES = new Set([
29876
+ "toLocaleString",
29877
+ "toLocaleDateString",
29878
+ "toLocaleTimeString"
29879
+ ]);
29880
+ const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
29881
+ const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
29882
+ const INTL_FORMAT_METHOD_NAMES = new Set([
29883
+ "format",
29884
+ "formatToParts",
29885
+ "formatRange"
29886
+ ]);
29887
+ const isProvableDateExpression = (expression) => {
29888
+ if (!expression) return false;
29889
+ const unwrapped = stripParenExpression(expression);
29890
+ return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
29891
+ };
29892
+ const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
29893
+ const receiverNameLooksDateFlavored = (expression) => {
29894
+ if (!expression) return false;
29895
+ const unwrapped = stripParenExpression(expression);
29896
+ if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
29897
+ if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
29898
+ if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
29899
+ return false;
29900
+ };
29901
+ const objectLiteralHasProperty = (objectExpression, propertyName) => {
29902
+ if (!objectExpression) return false;
29903
+ const unwrapped = stripParenExpression(objectExpression);
29904
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
29905
+ for (const property of unwrapped.properties ?? []) {
29906
+ if (!isNodeOfType(property, "Property")) continue;
29907
+ if (property.computed) continue;
29908
+ if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
29909
+ if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
29910
+ }
29911
+ return false;
29912
+ };
29913
+ const hasExplicitLocaleArgument = (argument) => {
29914
+ if (!argument) return false;
29915
+ const unwrapped = stripParenExpression(argument);
29916
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
29917
+ return true;
29918
+ };
29919
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
29920
+ const localeArgument = call.arguments?.[0];
29921
+ if (!hasExplicitLocaleArgument(localeArgument)) return false;
29922
+ const optionsArgument = call.arguments?.[1];
29923
+ if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
29924
+ return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
29925
+ };
29926
+ const matchLocaleMethodCall = (call) => {
29927
+ const callee = call.callee;
29928
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29929
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29930
+ const methodName = callee.property.name;
29931
+ if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
29932
+ const receiverIsProvablyDate = isProvableDateExpression(callee.object);
29933
+ if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
29934
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
29935
+ return {
29936
+ node: call,
29937
+ display: `${methodName}()`
29938
+ };
29939
+ };
29940
+ const getIntlFormatterName = (expression) => {
29941
+ if (!expression) return null;
29942
+ const unwrapped = stripParenExpression(expression);
29943
+ if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
29944
+ const callee = unwrapped.callee;
29945
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29946
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
29947
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29948
+ return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
29949
+ };
29950
+ const isDeterministicIntlConstruction = (construction, formatterName) => {
29951
+ if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
29952
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
29953
+ if (formatterName !== "DateTimeFormat") return true;
29954
+ return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
29955
+ };
29956
+ const matchIntlFormatCall = (call) => {
29957
+ const callee = call.callee;
29958
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29959
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29960
+ if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
29961
+ let construction = stripParenExpression(callee.object);
29962
+ if (isNodeOfType(construction, "Identifier")) {
29963
+ const binding = findVariableInitializer(construction, construction.name);
29964
+ construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
29965
+ }
29966
+ if (!construction) return null;
29967
+ const formatterName = getIntlFormatterName(construction);
29968
+ if (!formatterName) return null;
29969
+ if (isDeterministicIntlConstruction(construction, formatterName)) return null;
29970
+ return {
29971
+ node: call,
29972
+ display: `Intl.${formatterName}().${callee.property.name}()`
29973
+ };
29974
+ };
29975
+ const isDeterministicInputDateConstruction = (expression) => {
29976
+ if (!expression) return false;
29977
+ const unwrapped = stripParenExpression(expression);
29978
+ if (!isNodeOfType(unwrapped, "NewExpression")) return false;
29979
+ if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
29980
+ return (unwrapped.arguments?.length ?? 0) > 0;
29981
+ };
29982
+ const matchDateDefaultStringification = (node) => {
29983
+ if (isNodeOfType(node, "CallExpression")) {
29984
+ const callee = node.callee;
29985
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
29986
+ node,
29987
+ display: "Date.prototype.toString()"
29988
+ };
29989
+ if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
29990
+ node,
29991
+ display: "String(new Date(…))"
29992
+ };
29993
+ return null;
29994
+ }
29995
+ if (isNodeOfType(node, "TemplateLiteral")) {
29996
+ for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
29997
+ node: expression,
29998
+ display: "`${new Date(…)}`"
29999
+ };
30000
+ }
30001
+ return null;
30002
+ };
30003
+ const findRenderPhaseComponentOrHook = (node) => {
30004
+ let functionNode = findEnclosingFunction(node);
30005
+ while (functionNode) {
30006
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
30007
+ if (!executesDuringRender(functionNode)) return null;
30008
+ functionNode = findEnclosingFunction(functionNode);
30009
+ }
30010
+ return null;
30011
+ };
30012
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30013
+ if (fileHasUseClientDirective) return true;
30014
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30015
+ if (displayName && isReactHookName(displayName)) return true;
30016
+ let callsHook = false;
30017
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30018
+ if (callsHook) return false;
30019
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30020
+ callsHook = true;
30021
+ return false;
30022
+ }
30023
+ });
30024
+ return callsHook;
30025
+ };
30026
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
30027
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30028
+ if (!isNodeOfType(body, "BlockStatement")) return false;
30029
+ const ancestors = /* @__PURE__ */ new Set();
30030
+ let cursor = node;
30031
+ while (cursor) {
30032
+ ancestors.add(cursor);
30033
+ cursor = cursor.parent ?? null;
30034
+ }
30035
+ for (const statement of body.body ?? []) {
30036
+ if (ancestors.has(statement)) return false;
30037
+ if (!isNodeOfType(statement, "IfStatement")) continue;
30038
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
30039
+ let returnsEarly = false;
30040
+ walkAst(statement.consequent, (child) => {
30041
+ if (isFunctionLike$1(child)) return false;
30042
+ if (isNodeOfType(child, "ReturnStatement")) {
30043
+ returnsEarly = true;
30044
+ return false;
30045
+ }
30046
+ });
30047
+ if (returnsEarly) return true;
30048
+ }
30049
+ return false;
30050
+ };
30051
+ const findEnclosingJsxOpeningElement = (node) => {
30052
+ let cursor = node.parent;
30053
+ while (cursor) {
30054
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30055
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
30056
+ cursor = cursor.parent ?? null;
30057
+ }
30058
+ return null;
30059
+ };
30060
+ const noLocaleFormatInRender = defineRule({
30061
+ id: "no-locale-format-in-render",
30062
+ title: "Locale/timezone formatting during render",
30063
+ severity: "warn",
30064
+ category: "Correctness",
30065
+ disabledWhen: [
30066
+ "vite",
30067
+ "cra",
30068
+ "expo",
30069
+ "react-native",
30070
+ "unknown"
30071
+ ],
30072
+ recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
30073
+ create: (context) => {
30074
+ if (isTestlikeFilename(context.filename)) return {};
30075
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
30076
+ let fileHasUseClientDirective = false;
30077
+ let fileIsEmailTemplate = false;
30078
+ const reportedNodes = /* @__PURE__ */ new Set();
30079
+ const reportIfRenderPhase = (match) => {
30080
+ if (reportedNodes.has(match.node)) return;
30081
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30082
+ if (!componentOrHookNode) return;
30083
+ if (fileIsEmailTemplate) return;
30084
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30085
+ if (isInsideClientOnlyGuard(match.node)) return;
30086
+ if (isGatedByFalsyInitialState(match.node)) return;
30087
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30088
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30089
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30090
+ reportedNodes.add(match.node);
30091
+ context.report({
30092
+ node: match.node,
30093
+ message: `This can cause a hydration mismatch because ${match.display} formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
30094
+ });
30095
+ };
30096
+ return {
30097
+ Program(node) {
30098
+ fileHasUseClientDirective = hasDirective(node, "use client");
30099
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30100
+ },
30101
+ CallExpression(node) {
30102
+ const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30103
+ if (match) reportIfRenderPhase(match);
30104
+ },
30105
+ TemplateLiteral(node) {
30106
+ const match = matchDateDefaultStringification(node);
30107
+ if (match) reportIfRenderPhase(match);
30108
+ },
30109
+ JSXExpressionContainer(node) {
30110
+ const expression = stripParenExpression(node.expression);
30111
+ if (!isNodeOfType(expression, "CallExpression")) return;
30112
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
30113
+ const helperName = expression.callee.name;
30114
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30115
+ if (!componentOrHookNode) return;
30116
+ const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30117
+ if (!helperNode || !isFunctionLike$1(helperNode)) return;
30118
+ if (componentOrHookDisplayNameForFunction(helperNode)) return;
30119
+ walkAst(helperNode.body ?? helperNode, (child) => {
30120
+ if (isFunctionLike$1(child)) return false;
30121
+ if (!isNodeOfType(child, "CallExpression")) return;
30122
+ const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30123
+ if (!match || reportedNodes.has(match.node)) return;
30124
+ if (fileIsEmailTemplate) return;
30125
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30126
+ if (isInsideClientOnlyGuard(node)) return;
30127
+ if (isGatedByFalsyInitialState(node)) return;
30128
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30129
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30130
+ reportedNodes.add(match.node);
30131
+ context.report({
30132
+ node: match.node,
30133
+ message: `This can cause a hydration mismatch because ${match.display} (reached from JSX through "${helperName}") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
30134
+ });
30135
+ });
30136
+ }
30137
+ };
30138
+ }
30139
+ });
30140
+ //#endregion
29366
30141
  //#region src/plugin/rules/design/no-long-transition-duration.ts
29367
30142
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
29368
30143
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -30079,40 +30854,6 @@ const noMutableInDeps = defineRule({
30079
30854
  }
30080
30855
  });
30081
30856
  //#endregion
30082
- //#region src/plugin/utils/is-result-discarded-call.ts
30083
- const isResultDiscardedCall = (callExpression) => {
30084
- let node = callExpression;
30085
- let parent = node.parent;
30086
- while (parent) {
30087
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
30088
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
30089
- if (isNodeOfType(parent, "ChainExpression")) {
30090
- node = parent;
30091
- parent = node.parent;
30092
- continue;
30093
- }
30094
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
30095
- node = parent;
30096
- parent = node.parent;
30097
- continue;
30098
- }
30099
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
30100
- node = parent;
30101
- parent = node.parent;
30102
- continue;
30103
- }
30104
- if (isNodeOfType(parent, "SequenceExpression")) {
30105
- const expressions = parent.expressions ?? [];
30106
- if (expressions[expressions.length - 1] !== node) return true;
30107
- node = parent;
30108
- parent = node.parent;
30109
- continue;
30110
- }
30111
- return false;
30112
- }
30113
- return false;
30114
- };
30115
- //#endregion
30116
30857
  //#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
30117
30858
  const LODASH_MUTATOR_NAMES = new Set([
30118
30859
  "set",
@@ -30554,7 +31295,7 @@ const noNestedComponentDefinition = defineRule({
30554
31295
  id: "no-nested-component-definition",
30555
31296
  title: "Component defined inside another component",
30556
31297
  tags: ["test-noise", "react-jsx-only"],
30557
- severity: "error",
31298
+ severity: "warn",
30558
31299
  category: "Correctness",
30559
31300
  recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
30560
31301
  create: (context) => {
@@ -31509,12 +32250,12 @@ const noPassDataToParent = defineRule({
31509
32250
  if (calleeNode === identifier) {
31510
32251
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
31511
32252
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
31512
- } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32253
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31513
32254
  if (!isWholePropsObjectReference(analysis, ref)) continue;
31514
32255
  if (isCustomHookParameter(ref)) continue;
31515
32256
  } else continue;
31516
32257
  const methodName = getCallMethodName(calleeNode);
31517
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32258
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31518
32259
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31519
32260
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
31520
32261
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -31705,7 +32446,7 @@ const noPassLiveStateToParent = defineRule({
31705
32446
  if (isCallResultConsumedAsArgument(callExpr)) continue;
31706
32447
  const calleeNode = callExpr.callee;
31707
32448
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
31708
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32449
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31709
32450
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31710
32451
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
31711
32452
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -32239,7 +32980,7 @@ const isAlwaysFreshExpression = (expression) => {
32239
32980
  return `${callee.name}()`;
32240
32981
  }
32241
32982
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32242
- const receiver = callee.object;
32983
+ const receiver = stripParenExpression(callee.object);
32243
32984
  const property = callee.property;
32244
32985
  if (!isNodeOfType(property, "Identifier")) return null;
32245
32986
  if (isNodeOfType(receiver, "Identifier")) {
@@ -32360,8 +33101,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32360
33101
  if (typeof sourceValue !== "string") return;
32361
33102
  if (handleExtraSource?.(node, context)) return;
32362
33103
  if (sourceValue !== source) return;
33104
+ if (isTypeOnlyImport(node)) return;
32363
33105
  for (const specifier of node.specifiers ?? []) {
32364
33106
  if (isNodeOfType(specifier, "ImportSpecifier")) {
33107
+ if (specifier.importKind === "type") continue;
32365
33108
  const importedName = getImportedName$1(specifier);
32366
33109
  if (!importedName) continue;
32367
33110
  const message = messages.get(importedName);
@@ -32380,8 +33123,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32380
33123
  MemberExpression(node) {
32381
33124
  if (namespaceBindings.size === 0) return;
32382
33125
  if (node.computed) return;
32383
- if (!isNodeOfType(node.object, "Identifier")) return;
32384
- if (!namespaceBindings.has(node.object.name)) return;
33126
+ const receiver = stripParenExpression(node.object);
33127
+ if (!isNodeOfType(receiver, "Identifier")) return;
33128
+ if (!namespaceBindings.has(receiver.name)) return;
32385
33129
  if (!isNodeOfType(node.property, "Identifier")) return;
32386
33130
  const message = messages.get(node.property.name);
32387
33131
  if (message) context.report({
@@ -32446,7 +33190,7 @@ const noReactDomDeprecatedApis = defineRule({
32446
33190
  });
32447
33191
  //#endregion
32448
33192
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
32449
- const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
33193
+ const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
32450
33194
  const isVendoredShadcnUiFilename = (rawFilename) => {
32451
33195
  if (!rawFilename) return false;
32452
33196
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32484,7 +33228,7 @@ const noReact19DeprecatedApis = defineRule({
32484
33228
  requires: ["react:19"],
32485
33229
  tags: ["test-noise", "migration-hint"],
32486
33230
  severity: "warn",
32487
- recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
33231
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32488
33232
  create: (context) => {
32489
33233
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32490
33234
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -32808,34 +33552,11 @@ const noRedundantShouldComponentUpdate = defineRule({
32808
33552
  });
32809
33553
  //#endregion
32810
33554
  //#region src/plugin/rules/architecture/no-render-in-render.ts
32811
- const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32812
- if (!symbol || visitedSymbols.has(symbol)) return false;
32813
- visitedSymbols.add(symbol);
32814
- if (isComponentParameterSymbol(symbol)) return true;
32815
- if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
32816
- const source = symbol.initializer;
32817
- if (!source) return false;
32818
- return initializerRootsInProps(source, scopes, visitedSymbols);
32819
- };
32820
- const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32821
- if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
32822
- if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
32823
- return rootsInProps(node, scopes, visitedSymbols);
32824
- };
32825
- const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32826
- let current = node;
32827
- while (isNodeOfType(current, "MemberExpression")) {
32828
- if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
32829
- current = current.object;
32830
- }
32831
- if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
32832
- return false;
32833
- };
32834
33555
  const isInsideComponentContext = (node) => {
32835
33556
  let cursor = node.parent;
32836
33557
  while (cursor) {
32837
- if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
32838
33558
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33559
+ if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
32839
33560
  cursor = cursor.parent ?? null;
32840
33561
  }
32841
33562
  return false;
@@ -32845,24 +33566,28 @@ const functionBodyOf = (node) => {
32845
33566
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
32846
33567
  return null;
32847
33568
  };
33569
+ const isHookCallee = (callee) => {
33570
+ if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
33571
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
33572
+ return false;
33573
+ };
32848
33574
  const containsHookCall = (body) => {
32849
33575
  let found = false;
32850
33576
  walkAst(body, (child) => {
32851
- if (found) return;
33577
+ if (found) return false;
33578
+ if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32852
33579
  if (!isNodeOfType(child, "CallExpression")) return;
32853
- const name = getCalleeName$2(child);
32854
- if (name && isReactHookName(name)) found = true;
33580
+ if (isHookCallee(child.callee)) found = true;
32855
33581
  });
32856
33582
  return found;
32857
33583
  };
32858
- const isModuleScopeHookFreeHelper = (symbol) => {
33584
+ const isHookCallingRenderHelper = (symbol) => {
32859
33585
  if (!symbol) return false;
32860
33586
  const declaration = symbol.declarationNode;
32861
33587
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
32862
33588
  const body = functionBodyOf(declaration);
32863
33589
  if (!body) return false;
32864
- if (findEnclosingFunction(declaration) !== null) return false;
32865
- return !containsHookCall(body);
33590
+ return containsHookCall(body);
32866
33591
  };
32867
33592
  const noRenderInRender = defineRule({
32868
33593
  id: "no-render-in-render",
@@ -32871,20 +33596,13 @@ const noRenderInRender = defineRule({
32871
33596
  tags: ["test-noise"],
32872
33597
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
32873
33598
  create: (context) => ({ JSXExpressionContainer(node) {
32874
- const expression = node.expression;
33599
+ const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
32875
33600
  if (!isNodeOfType(expression, "CallExpression")) return;
32876
- let calleeName = null;
32877
- if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
32878
- else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
32879
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33601
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
33602
+ const calleeName = expression.callee.name;
33603
+ if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32880
33604
  if (!isInsideComponentContext(node)) return;
32881
- if (isNodeOfType(expression.callee, "Identifier")) {
32882
- const calleeSymbol = context.scopes.symbolFor(expression.callee);
32883
- if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
32884
- if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
32885
- } else if (isNodeOfType(expression.callee, "MemberExpression")) {
32886
- if (rootsInProps(expression.callee.object, context.scopes)) return;
32887
- }
33605
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32888
33606
  context.report({
32889
33607
  node: expression,
32890
33608
  message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
@@ -32945,13 +33663,7 @@ const noRenderPropChildren = defineRule({
32945
33663
  //#endregion
32946
33664
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
32947
33665
  const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
32948
- const isReactDomRenderCall = (node) => {
32949
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
32950
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
32951
- if (node.callee.object.name !== "ReactDOM") return false;
32952
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
32953
- return node.callee.property.name === "render";
32954
- };
33666
+ const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
32955
33667
  const isUsedAsReturnValue = (parent) => {
32956
33668
  if (!parent) return false;
32957
33669
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -33787,7 +34499,7 @@ const noSetState = defineRule({
33787
34499
  category: "Architecture",
33788
34500
  create: (context) => ({ CallExpression(node) {
33789
34501
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
33790
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
34502
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33791
34503
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
33792
34504
  if (!getParentComponent(node)) return;
33793
34505
  context.report({
@@ -33953,6 +34665,204 @@ const noSideTabBorder = defineRule({
33953
34665
  })
33954
34666
  });
33955
34667
  //#endregion
34668
+ //#region src/plugin/rules/state-and-effects/no-stale-timer-ref.ts
34669
+ const GLOBAL_TIMER_RECEIVER_NAMES = new Set([
34670
+ "window",
34671
+ "globalThis",
34672
+ "self"
34673
+ ]);
34674
+ const NULLISH_COMPARISON_OPERATORS = new Set([
34675
+ "==",
34676
+ "===",
34677
+ "!=",
34678
+ "!=="
34679
+ ]);
34680
+ const getGlobalTimerCalleeName = (node) => {
34681
+ if (!isNodeOfType(node, "CallExpression")) return null;
34682
+ const callee = stripParenExpression(node.callee);
34683
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
34684
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
34685
+ const receiver = stripParenExpression(callee.object);
34686
+ if (isNodeOfType(receiver, "Identifier") && GLOBAL_TIMER_RECEIVER_NAMES.has(receiver.name)) return callee.property.name;
34687
+ }
34688
+ return null;
34689
+ };
34690
+ const getRefCurrentReceiverName = (node) => {
34691
+ if (!isNodeOfType(node, "MemberExpression") || node.computed) return null;
34692
+ if (!isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
34693
+ const receiver = stripParenExpression(node.object);
34694
+ return isNodeOfType(receiver, "Identifier") ? receiver.name : null;
34695
+ };
34696
+ const isRefCurrentMemberOf = (node, refName) => getRefCurrentReceiverName(node) === refName;
34697
+ const parseClearCallOnTimerRef = (node) => {
34698
+ const clearCalleeName = getGlobalTimerCalleeName(node);
34699
+ if (clearCalleeName === null || !TIMER_CLEANUP_CALLEE_NAMES.has(clearCalleeName)) return null;
34700
+ if (!isNodeOfType(node, "CallExpression")) return null;
34701
+ const clearedArgument = node.arguments?.[0];
34702
+ if (!clearedArgument) return null;
34703
+ const refName = getRefCurrentReceiverName(stripParenExpression(clearedArgument));
34704
+ return refName === null ? null : {
34705
+ clearCalleeName,
34706
+ refName
34707
+ };
34708
+ };
34709
+ const isClearCallOnRef = (node, refName) => parseClearCallOnTimerRef(node)?.refName === refName;
34710
+ const isNullishResetExpression = (node) => isNullishExpression(stripParenExpression(node));
34711
+ const isAssignmentToRefCurrent = (node, refName) => isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isRefCurrentMemberOf(node.left, refName);
34712
+ const isClearGuardIfIdiom = (ifStatement, refName) => {
34713
+ if (ifStatement.alternate) return false;
34714
+ const consequent = ifStatement.consequent;
34715
+ return (isNodeOfType(consequent, "BlockStatement") ? consequent.body ?? [] : [consequent]).every((statement) => {
34716
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
34717
+ const expression = stripParenExpression(statement.expression);
34718
+ if (isClearCallOnRef(expression, refName)) return true;
34719
+ return isAssignmentToRefCurrent(expression, refName) && isNullishResetExpression(expression.right);
34720
+ });
34721
+ };
34722
+ const climbBooleanProjection = (refCurrentMember) => {
34723
+ let cursor = refCurrentMember;
34724
+ const logicalAncestors = [];
34725
+ let didPassBooleanProjection = false;
34726
+ while (cursor.parent) {
34727
+ const parent = cursor.parent;
34728
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type)) {
34729
+ cursor = parent;
34730
+ continue;
34731
+ }
34732
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
34733
+ didPassBooleanProjection = true;
34734
+ cursor = parent;
34735
+ continue;
34736
+ }
34737
+ if (isNodeOfType(parent, "BinaryExpression") && NULLISH_COMPARISON_OPERATORS.has(parent.operator) && isNullishResetExpression(parent.left === cursor ? parent.right : parent.left)) {
34738
+ didPassBooleanProjection = true;
34739
+ cursor = parent;
34740
+ continue;
34741
+ }
34742
+ if (isNodeOfType(parent, "LogicalExpression")) {
34743
+ logicalAncestors.push(parent);
34744
+ cursor = parent;
34745
+ continue;
34746
+ }
34747
+ break;
34748
+ }
34749
+ return {
34750
+ conditionRoot: cursor,
34751
+ logicalAncestors,
34752
+ didPassBooleanProjection
34753
+ };
34754
+ };
34755
+ const isLogicalClearGuardIdiom = (logicalAncestors, refName) => logicalAncestors.some((logical) => logical.operator === "&&" && isClearCallOnRef(stripParenExpression(logical.right), refName));
34756
+ const isPendingSignalRead = (refCurrentMember, refName) => {
34757
+ const { conditionRoot, logicalAncestors, didPassBooleanProjection } = climbBooleanProjection(refCurrentMember);
34758
+ const conditionParent = conditionRoot.parent;
34759
+ if (isNodeOfType(conditionParent, "IfStatement") && conditionParent.test === conditionRoot) return !isClearGuardIfIdiom(conditionParent, refName);
34760
+ if ((isNodeOfType(conditionParent, "ConditionalExpression") || isNodeOfType(conditionParent, "WhileStatement") || isNodeOfType(conditionParent, "DoWhileStatement") || isNodeOfType(conditionParent, "ForStatement")) && conditionParent.test === conditionRoot) return true;
34761
+ if (logicalAncestors.length > 0) return !isLogicalClearGuardIdiom(logicalAncestors, refName);
34762
+ return didPassBooleanProjection;
34763
+ };
34764
+ const collectTimerRefUsageFacts = (ownerScope, refName) => {
34765
+ const facts = {
34766
+ holdsScheduledTimerId: false,
34767
+ hasPendingSignalRead: false
34768
+ };
34769
+ walkAst(ownerScope, (child) => {
34770
+ if (isAssignmentToRefCurrent(child, refName)) {
34771
+ const assignedCalleeName = getGlobalTimerCalleeName(stripParenExpression(child.right));
34772
+ if (assignedCalleeName !== null && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(assignedCalleeName)) facts.holdsScheduledTimerId = true;
34773
+ return;
34774
+ }
34775
+ if (isRefCurrentMemberOf(child, refName) && isPendingSignalRead(child, refName)) facts.hasPendingSignalRead = true;
34776
+ });
34777
+ return facts;
34778
+ };
34779
+ const isEffectCallbackFunction = (functionNode) => {
34780
+ const parent = functionNode.parent;
34781
+ if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
34782
+ return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
34783
+ };
34784
+ const doesEffectCallbackReturnName = (effectCallback, name) => {
34785
+ if (!isFunctionLike$1(effectCallback)) return false;
34786
+ let isReturnedByName = false;
34787
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
34788
+ if (isReturnedByName || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
34789
+ const returned = stripParenExpression(child.argument);
34790
+ if (isNodeOfType(returned, "Identifier") && returned.name === name) isReturnedByName = true;
34791
+ });
34792
+ return isReturnedByName;
34793
+ };
34794
+ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
34795
+ if (!isFunctionLike$1(effectCallback)) return false;
34796
+ if (isNodeOfType(functionNode.parent, "ReturnStatement")) return true;
34797
+ if (effectCallback.body === functionNode) return true;
34798
+ const cleanupBindingName = getFunctionBindingName$1(functionNode);
34799
+ return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
34800
+ };
34801
+ const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
34802
+ const cleanupBindingName = getFunctionBindingName$1(functionNode);
34803
+ if (cleanupBindingName === null) return false;
34804
+ let isReturnedFromEffect = false;
34805
+ walkAst(ownerScope, (child) => {
34806
+ if (isReturnedFromEffect) return false;
34807
+ if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
34808
+ const effectCallback = getEffectCallback(child);
34809
+ if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
34810
+ });
34811
+ return isReturnedFromEffect;
34812
+ };
34813
+ const isInsideEffectCleanupReturn = (node, ownerScope) => {
34814
+ let functionNode = findEnclosingFunction(node);
34815
+ while (functionNode) {
34816
+ const outerFunction = findEnclosingFunction(functionNode);
34817
+ if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
34818
+ if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
34819
+ functionNode = outerFunction;
34820
+ }
34821
+ return false;
34822
+ };
34823
+ const hasRefCurrentReassignmentAfterClear = (clearCall, refName) => {
34824
+ const enclosingFunction = findEnclosingFunction(clearCall);
34825
+ if (!isFunctionLike$1(enclosingFunction)) return false;
34826
+ if (!isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
34827
+ const clearStart = getRangeStart(clearCall);
34828
+ if (clearStart === null) return false;
34829
+ let didFindLaterReassignment = false;
34830
+ walkInsideStatementBlocks(enclosingFunction.body, (child) => {
34831
+ if (didFindLaterReassignment) return;
34832
+ if (!isAssignmentToRefCurrent(child, refName)) return;
34833
+ const assignmentStart = getRangeStart(child);
34834
+ if (assignmentStart !== null && assignmentStart > clearStart) didFindLaterReassignment = true;
34835
+ });
34836
+ return didFindLaterReassignment;
34837
+ };
34838
+ const isShadowedTimerGlobal = (clearCall) => {
34839
+ const callee = stripParenExpression(clearCall.callee);
34840
+ if (!isNodeOfType(callee, "Identifier")) return false;
34841
+ return findVariableInitializer(callee, callee.name) !== null;
34842
+ };
34843
+ const noStaleTimerRef = defineRule({
34844
+ id: "no-stale-timer-ref",
34845
+ title: "Cleared timer ref keeps the stale id",
34846
+ severity: "warn",
34847
+ recommendation: "Reset the ref right after clearing (`clearTimeout(ref.current); ref.current = null`) so truthiness checks on the ref keep meaning “timer still pending”.",
34848
+ create: (context) => ({ CallExpression(node) {
34849
+ const clearCall = parseClearCallOnTimerRef(node);
34850
+ if (!clearCall) return;
34851
+ if (isShadowedTimerGlobal(node)) return;
34852
+ const { clearCalleeName, refName } = clearCall;
34853
+ const refBinding = findVariableInitializer(node, refName);
34854
+ if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
34855
+ const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
34856
+ if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
34857
+ if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
34858
+ if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
34859
+ context.report({
34860
+ node,
34861
+ message: `\`${clearCalleeName}(${refName}.current)\` cancels the timer but leaves the old id in \`${refName}.current\`, and this component reads \`${refName}.current\` as a \u201Ctimer pending\u201D signal — assign \`${refName}.current = null\` right after clearing so a cancelled timer does not look pending.`
34862
+ });
34863
+ } })
34864
+ });
34865
+ //#endregion
33956
34866
  //#region src/plugin/utils/is-abstract-role.ts
33957
34867
  const isAbstractRole = (openingElement, settings) => {
33958
34868
  const elementType = getElementType(openingElement, settings);
@@ -36233,6 +37143,36 @@ const isTriviallyCheapExpression = (node) => {
36233
37143
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36234
37144
  return true;
36235
37145
  };
37146
+ const isTrivialContainerLiteral = (node) => {
37147
+ if (!node) return false;
37148
+ const innerExpression = stripParenExpression(node);
37149
+ if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
37150
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
37151
+ return false;
37152
+ };
37153
+ const isNonEscapingRead = (identifier) => {
37154
+ const readRoot = findTransparentExpressionRoot(identifier);
37155
+ const memberNode = readRoot.parent;
37156
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
37157
+ const memberUse = findTransparentExpressionRoot(memberNode);
37158
+ const memberUseParent = memberUse.parent;
37159
+ if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
37160
+ if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
37161
+ if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
37162
+ return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
37163
+ };
37164
+ const isMemoIdentityUnused = (memoCallNode, scopes) => {
37165
+ const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
37166
+ const parentNode = memoUsageRoot.parent;
37167
+ if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
37168
+ if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
37169
+ const bindingTarget = parentNode.id;
37170
+ if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
37171
+ if (!isNodeOfType(bindingTarget, "Identifier")) return false;
37172
+ const symbol = scopes.symbolFor(bindingTarget);
37173
+ if (!symbol) return false;
37174
+ return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
37175
+ };
36236
37176
  const noUsememoSimpleExpression = defineRule({
36237
37177
  id: "no-usememo-simple-expression",
36238
37178
  title: "useMemo on a cheap value",
@@ -36254,9 +37194,17 @@ const noUsememoSimpleExpression = defineRule({
36254
37194
  let returnExpression = null;
36255
37195
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36256
37196
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36257
- if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
37197
+ if (!returnExpression) return;
37198
+ if (isTriviallyCheapExpression(returnExpression)) {
37199
+ context.report({
37200
+ node,
37201
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37202
+ });
37203
+ return;
37204
+ }
37205
+ if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36258
37206
  node,
36259
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37207
+ message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36260
37208
  });
36261
37209
  } })
36262
37210
  });
@@ -36362,7 +37310,7 @@ const noWillUpdateSetState = defineRule({
36362
37310
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
36363
37311
  return { CallExpression(node) {
36364
37312
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
36365
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
37313
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36366
37314
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36367
37315
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36368
37316
  context.report({
@@ -36679,6 +37627,10 @@ const isRouteFactoryCall = (expression) => {
36679
37627
  }
36680
37628
  return false;
36681
37629
  };
37630
+ const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
37631
+ const expression = skipTsExpression(argument);
37632
+ return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
37633
+ });
36682
37634
  const isReactHocName = (name, state) => state.customHocs.has(name);
36683
37635
  const isHocCallee = (callee, state) => {
36684
37636
  if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
@@ -36718,7 +37670,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
36718
37670
  continue;
36719
37671
  }
36720
37672
  if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
36721
- if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
37673
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
36722
37674
  if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
36723
37675
  }
36724
37676
  return false;
@@ -36826,18 +37778,22 @@ const onlyExportComponents = defineRule({
36826
37778
  customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36827
37779
  allowExportNames: new Set(settings.allowExportNames),
36828
37780
  allowConstantExport: settings.allowConstantExport,
36829
- localComponentNames
37781
+ localComponentNames,
37782
+ scopes: context.scopes
36830
37783
  };
36831
37784
  for (const child of componentCandidates) {
36832
37785
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36833
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37786
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
36834
37787
  }
36835
37788
  if (isNodeOfType(child, "ClassDeclaration") && child.id) {
36836
37789
  if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36837
37790
  }
36838
37791
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36839
37792
  const initializer = child.init;
36840
- if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37793
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
37794
+ const expression = initializer ? skipTsExpression(initializer) : null;
37795
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
37796
+ }
36841
37797
  }
36842
37798
  }
36843
37799
  const exports = [];
@@ -36907,6 +37863,10 @@ const onlyExportComponents = defineRule({
36907
37863
  return false;
36908
37864
  })();
36909
37865
  if (isHoc && firstArgIsValid) hasReactExport = true;
37866
+ else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
37867
+ kind: "non-component",
37868
+ reportNode: stripped
37869
+ });
36910
37870
  else context.report({
36911
37871
  node: stripped,
36912
37872
  message: ANONYMOUS_MESSAGE
@@ -38502,11 +39462,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
38502
39462
  }
38503
39463
  return null;
38504
39464
  };
39465
+ const isTrivialLiteralExpression = (expression) => {
39466
+ if (isNodeOfType(expression, "Literal")) return true;
39467
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
39468
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
39469
+ if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
39470
+ return false;
39471
+ };
38505
39472
  const getSingleSetterCallFromHandler = (handler) => {
38506
39473
  const handlerStatements = getCallbackStatements(handler);
38507
39474
  if (handlerStatements.length !== 1) return null;
38508
39475
  const onlyStatement = handlerStatements[0];
38509
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
39476
+ let expression = onlyStatement;
39477
+ if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
39478
+ if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
39479
+ if (!expression) return null;
39480
+ expression = stripParenExpression(expression);
38510
39481
  if (!isNodeOfType(expression, "CallExpression")) return null;
38511
39482
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
38512
39483
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -38516,6 +39487,106 @@ const getSingleSetterCallFromHandler = (handler) => {
38516
39487
  setterArgument: expression.arguments[0]
38517
39488
  };
38518
39489
  };
39490
+ const isListenerCollectionInitializer = (init) => {
39491
+ if (!init) return false;
39492
+ if (isNodeOfType(init, "ArrayExpression")) return true;
39493
+ return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
39494
+ };
39495
+ const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
39496
+ if (!isFunctionLike$1(functionNode)) return false;
39497
+ const firstParam = functionNode.params?.[0];
39498
+ if (!isNodeOfType(firstParam, "Identifier")) return false;
39499
+ const listenerParamName = firstParam.name;
39500
+ let registersListener = false;
39501
+ walkAst(functionNode.body, (child) => {
39502
+ if (registersListener) return false;
39503
+ if (!isNodeOfType(child, "CallExpression")) return;
39504
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
39505
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
39506
+ if (!listenerCollectionNames.has(child.callee.object.name)) return;
39507
+ if (!isNodeOfType(child.callee.property, "Identifier")) return;
39508
+ if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
39509
+ const registeredArgument = child.arguments?.[0];
39510
+ if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
39511
+ });
39512
+ return registersListener;
39513
+ };
39514
+ const buildModuleScopeStoreIndex = (programRoot) => {
39515
+ const mutableBindingNames = /* @__PURE__ */ new Set();
39516
+ const listenerCollectionNames = /* @__PURE__ */ new Set();
39517
+ const moduleFunctionsByName = /* @__PURE__ */ new Map();
39518
+ if (!isNodeOfType(programRoot, "Program")) return {
39519
+ mutableBindingNames,
39520
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39521
+ };
39522
+ for (const statement of programRoot.body ?? []) {
39523
+ const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
39524
+ if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
39525
+ moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
39526
+ continue;
39527
+ }
39528
+ if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
39529
+ for (const declarator of unwrapped.declarations ?? []) {
39530
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
39531
+ const init = declarator.init ?? null;
39532
+ if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
39533
+ mutableBindingNames.add(declarator.id.name);
39534
+ continue;
39535
+ }
39536
+ if (isListenerCollectionInitializer(init)) {
39537
+ listenerCollectionNames.add(declarator.id.name);
39538
+ continue;
39539
+ }
39540
+ if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
39541
+ }
39542
+ }
39543
+ const subscribeFunctionNames = /* @__PURE__ */ new Set();
39544
+ for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
39545
+ return {
39546
+ mutableBindingNames,
39547
+ subscribeFunctionNames
39548
+ };
39549
+ };
39550
+ const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
39551
+ if (!isNodeOfType(useStateCall, "CallExpression")) return null;
39552
+ let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
39553
+ if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
39554
+ if (!isNodeOfType(initialArgument, "Identifier")) return null;
39555
+ if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
39556
+ const binding = findVariableInitializer(initialArgument, initialArgument.name);
39557
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
39558
+ return initialArgument.name;
39559
+ };
39560
+ const argumentForwardsSetter = (argument, setterName) => {
39561
+ if (!argument) return false;
39562
+ const unwrapped = stripParenExpression(argument);
39563
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
39564
+ if (!isFunctionLike$1(unwrapped)) return false;
39565
+ let callsSetter = false;
39566
+ walkAst(unwrapped.body, (child) => {
39567
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
39568
+ callsSetter = true;
39569
+ return false;
39570
+ }
39571
+ });
39572
+ return callsSetter;
39573
+ };
39574
+ const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
39575
+ let matchedCall = null;
39576
+ walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
39577
+ if (matchedCall) return false;
39578
+ if (!isNodeOfType(child, "CallExpression")) return;
39579
+ if (!isNodeOfType(child.callee, "Identifier")) return;
39580
+ if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
39581
+ const binding = findVariableInitializer(child.callee, child.callee.name);
39582
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
39583
+ for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
39584
+ matchedCall = child;
39585
+ return false;
39586
+ }
39587
+ });
39588
+ return matchedCall;
39589
+ };
38519
39590
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
38520
39591
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
38521
39592
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -38532,6 +39603,16 @@ const preferUseSyncExternalStore = defineRule({
38532
39603
  severity: "warn",
38533
39604
  recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
38534
39605
  create: (context) => {
39606
+ let cachedStoreIndex = null;
39607
+ const storeIndexFor = (node) => {
39608
+ if (cachedStoreIndex) return cachedStoreIndex;
39609
+ const programRoot = findProgramRoot(node);
39610
+ cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
39611
+ mutableBindingNames: /* @__PURE__ */ new Set(),
39612
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39613
+ };
39614
+ return cachedStoreIndex;
39615
+ };
38535
39616
  const checkComponent = (componentBody) => {
38536
39617
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38537
39618
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -38569,6 +39650,7 @@ const preferUseSyncExternalStore = defineRule({
38569
39650
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
38570
39651
  if (!useStateInitializer) continue;
38571
39652
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
39653
+ if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
38572
39654
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
38573
39655
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
38574
39656
  context.report({
@@ -38576,14 +39658,47 @@ const preferUseSyncExternalStore = defineRule({
38576
39658
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
38577
39659
  });
38578
39660
  }
39661
+ checkModuleStoreShape(componentBody, useStateBindings);
39662
+ };
39663
+ const checkModuleStoreShape = (componentBody, useStateBindings) => {
39664
+ const storeIndex = storeIndexFor(componentBody);
39665
+ if (storeIndex.mutableBindingNames.size === 0) return;
39666
+ if (storeIndex.subscribeFunctionNames.size === 0) return;
39667
+ const snapshotBindings = useStateBindings.map((binding) => ({
39668
+ binding,
39669
+ storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
39670
+ })).filter((candidate) => candidate.storeName !== null);
39671
+ if (snapshotBindings.length === 0) return;
39672
+ const reportedDeclarators = /* @__PURE__ */ new Set();
39673
+ for (const effectCall of findUseEffectsInComponent(componentBody)) {
39674
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
39675
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
39676
+ const depsNode = effectCall.arguments[1];
39677
+ if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
39678
+ if ((depsNode.elements?.length ?? 0) !== 0) continue;
39679
+ const callback = getEffectCallback(effectCall);
39680
+ if (!callback || !isFunctionLike$1(callback)) continue;
39681
+ for (const { binding, storeName } of snapshotBindings) {
39682
+ if (reportedDeclarators.has(binding.declarator)) continue;
39683
+ if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
39684
+ reportedDeclarators.add(binding.declarator);
39685
+ context.report({
39686
+ node: binding.declarator,
39687
+ message: `Your users can miss updates or see torn values because useState "${binding.valueName}" snapshots module store "${storeName}" at render but only subscribes later in a useEffect.`
39688
+ });
39689
+ }
39690
+ }
38579
39691
  };
38580
39692
  return {
38581
39693
  FunctionDeclaration(node) {
38582
- if (!node.id?.name || !isUppercaseName(node.id.name)) return;
39694
+ const functionName = node.id?.name;
39695
+ if (!functionName) return;
39696
+ if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38583
39697
  checkComponent(node.body);
38584
39698
  },
38585
39699
  VariableDeclarator(node) {
38586
- if (!isComponentAssignment(node)) return;
39700
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
39701
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
38587
39702
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
38588
39703
  checkComponent(node.init.body);
38589
39704
  }
@@ -39193,7 +40308,7 @@ const queryNoVoidQueryFn = defineRule({
39193
40308
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
39194
40309
  const body = queryFnValue.body;
39195
40310
  if (!isNodeOfType(body, "BlockStatement")) return;
39196
- if ((body.body ?? []).length === 0) context.report({
40311
+ if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39197
40312
  node: queryFnProperty,
39198
40313
  message: "This empty queryFn caches undefined, so the component never gets data."
39199
40314
  });
@@ -39700,17 +40815,6 @@ const renderingHoistJsx = defineRule({
39700
40815
  }
39701
40816
  });
39702
40817
  //#endregion
39703
- //#region src/plugin/utils/find-declarator-for-binding.ts
39704
- const findDeclaratorForBinding = (bindingIdentifier) => {
39705
- let ancestor = bindingIdentifier.parent;
39706
- while (ancestor) {
39707
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
39708
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
39709
- ancestor = ancestor.parent ?? null;
39710
- }
39711
- return null;
39712
- };
39713
- //#endregion
39714
40818
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
39715
40819
  const NONDETERMINISTIC_RENDER_PATTERNS = [
39716
40820
  {
@@ -39719,19 +40823,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
39719
40823
  },
39720
40824
  {
39721
40825
  display: "Date.now()",
39722
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Date" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40826
+ matches: (node) => isGlobalMethodCall(node, "Date", "now")
39723
40827
  },
39724
40828
  {
39725
40829
  display: "Math.random()",
39726
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Math" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "random"
40830
+ matches: (node) => isGlobalMethodCall(node, "Math", "random")
39727
40831
  },
39728
40832
  {
39729
40833
  display: "performance.now()",
39730
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "performance" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40834
+ matches: (node) => isGlobalMethodCall(node, "performance", "now")
39731
40835
  },
39732
40836
  {
39733
40837
  display: "crypto.randomUUID()",
39734
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "crypto" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "randomUUID"
40838
+ matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39735
40839
  }
39736
40840
  ];
39737
40841
  const findOpeningElementOfChild = (jsxNode) => {
@@ -39743,54 +40847,6 @@ const findOpeningElementOfChild = (jsxNode) => {
39743
40847
  }
39744
40848
  return null;
39745
40849
  };
39746
- const executesDuringRender = (functionNode) => {
39747
- const parent = functionNode.parent;
39748
- if (!isNodeOfType(parent, "CallExpression")) return false;
39749
- if (parent.callee === functionNode) return true;
39750
- return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
39751
- };
39752
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
39753
- const referencesClientOnlyFlag = (expression) => {
39754
- const unwrapped = stripParenExpression(expression);
39755
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
39756
- if (isNodeOfType(unwrapped, "MemberExpression")) {
39757
- const property = unwrapped.property;
39758
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
39759
- }
39760
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
39761
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
39762
- return false;
39763
- };
39764
- const isFalsyLiteral = (node) => {
39765
- if (!node) return true;
39766
- if (isNodeOfType(node, "Literal")) return !node.value;
39767
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
39768
- };
39769
- const isFalsyInitialStateBinding = (identifier) => {
39770
- if (!isNodeOfType(identifier, "Identifier")) return false;
39771
- const binding = findVariableInitializer(identifier, identifier.name);
39772
- if (!binding) return false;
39773
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
39774
- if (!declarator?.init) return false;
39775
- const init = stripParenExpression(declarator.init);
39776
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
39777
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
39778
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
39779
- return isFalsyLiteral(init.arguments?.[0]);
39780
- };
39781
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
39782
- const isGatedByFalsyInitialState = (node) => {
39783
- let cursor = node;
39784
- let parent = node.parent;
39785
- while (parent) {
39786
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
39787
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39788
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39789
- cursor = parent;
39790
- parent = parent.parent ?? null;
39791
- }
39792
- return false;
39793
- };
39794
40850
  const isYearOnlyDateRead = (dateNode) => {
39795
40851
  const member = dateNode.parent;
39796
40852
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -39814,23 +40870,6 @@ const isInsideMotionTransitionAttribute = (node) => {
39814
40870
  }
39815
40871
  return false;
39816
40872
  };
39817
- const isInsideClientOnlyGuard = (node) => {
39818
- let cursor = node;
39819
- let parent = node.parent;
39820
- while (parent) {
39821
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
39822
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
39823
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
39824
- cursor = parent;
39825
- parent = parent.parent ?? null;
39826
- }
39827
- return false;
39828
- };
39829
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
39830
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
39831
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
39832
- return false;
39833
- };
39834
40873
  const renderingHydrationMismatchTime = defineRule({
39835
40874
  id: "rendering-hydration-mismatch-time",
39836
40875
  title: "Time or random value in JSX",
@@ -39878,6 +40917,35 @@ const renderingHydrationMismatchTime = defineRule({
39878
40917
  }
39879
40918
  });
39880
40919
  //#endregion
40920
+ //#region src/plugin/utils/contains-locale-environment-read.ts
40921
+ const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
40922
+ "toLocaleString",
40923
+ "toLocaleDateString",
40924
+ "toLocaleTimeString",
40925
+ "getTimezoneOffset"
40926
+ ]);
40927
+ const containsLocaleEnvironmentRead = (expression) => {
40928
+ let readsLocaleEnvironment = false;
40929
+ walkAst(expression, (child) => {
40930
+ if (readsLocaleEnvironment) return false;
40931
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
40932
+ if (child.object.name === "Intl") {
40933
+ readsLocaleEnvironment = true;
40934
+ return false;
40935
+ }
40936
+ if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
40937
+ readsLocaleEnvironment = true;
40938
+ return false;
40939
+ }
40940
+ }
40941
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && LOCALE_ENVIRONMENT_METHOD_NAMES.has(child.callee.property.name)) {
40942
+ readsLocaleEnvironment = true;
40943
+ return false;
40944
+ }
40945
+ });
40946
+ return readsLocaleEnvironment;
40947
+ };
40948
+ //#endregion
39881
40949
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
39882
40950
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
39883
40951
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -39943,14 +41011,15 @@ const renderingHydrationNoFlicker = defineRule({
39943
41011
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
39944
41012
  const callback = getEffectCallback(node);
39945
41013
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
39946
- const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
39947
- if (!bodyStatements || bodyStatements.length !== 1) return;
41014
+ const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
41015
+ if (bodyStatements.length !== 1) return;
39948
41016
  const soleStatement = bodyStatements[0];
39949
41017
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
39950
41018
  const expression = soleStatement.expression;
39951
41019
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
39952
41020
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
39953
41021
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
41022
+ if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
39954
41023
  context.report({
39955
41024
  node,
39956
41025
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -40769,6 +41838,9 @@ const rerenderFunctionalSetstate = defineRule({
40769
41838
  } })
40770
41839
  });
40771
41840
  //#endregion
41841
+ //#region src/plugin/utils/is-trivial-built-in-construction.ts
41842
+ const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
41843
+ //#endregion
40772
41844
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
40773
41845
  const rerenderLazyRefInit = defineRule({
40774
41846
  id: "rerender-lazy-ref-init",
@@ -40779,7 +41851,7 @@ const rerenderLazyRefInit = defineRule({
40779
41851
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
40780
41852
  create: (context) => ({ CallExpression(node) {
40781
41853
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
40782
- const initializer = node.arguments[0];
41854
+ const initializer = stripParenExpression(node.arguments[0]);
40783
41855
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
40784
41856
  const isNewCall = isNodeOfType(initializer, "NewExpression");
40785
41857
  if (!isPlainCall && !isNewCall) return;
@@ -40787,7 +41859,7 @@ const rerenderLazyRefInit = defineRule({
40787
41859
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40788
41860
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40789
41861
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40790
- if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41862
+ if (isTrivialBuiltInConstruction(initializer)) return;
40791
41863
  if (isPlainCall && isReactHookName(calleeName)) return;
40792
41864
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40793
41865
  context.report({
@@ -40823,11 +41895,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40823
41895
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40824
41896
  const findEagerInitializerCall = (expression, depth = 0) => {
40825
41897
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
40826
- if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
40827
- if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
40828
- if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
40829
- if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
40830
- if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
41898
+ const innerExpression = stripParenExpression(expression);
41899
+ if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
41900
+ if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
41901
+ if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
41902
+ if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
40831
41903
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
40832
41904
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
40833
41905
  if (spreadCall) return spreadCall;
@@ -40850,7 +41922,7 @@ const rerenderLazyStateInit = defineRule({
40850
41922
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40851
41923
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40852
41924
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40853
- if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41925
+ if (isTrivialBuiltInConstruction(initializer)) return;
40854
41926
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
40855
41927
  if (isReactHookName(calleeName)) return;
40856
41928
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -41517,7 +42589,7 @@ const rnAnimationReactionAsDerived = defineRule({
41517
42589
  const body = reactionFn.body;
41518
42590
  let singleAssignment = null;
41519
42591
  if (isNodeOfType(body, "BlockStatement")) {
41520
- const statements = body.body ?? [];
42592
+ const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41521
42593
  if (statements.length !== 1) return;
41522
42594
  const onlyStatement = statements[0];
41523
42595
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -41591,7 +42663,8 @@ const PROMISE_SETTLE_METHODS = new Set([
41591
42663
  "catch",
41592
42664
  "finally"
41593
42665
  ]);
41594
- const findChainRoot = (node) => {
42666
+ const findChainRoot = (wrappedNode) => {
42667
+ const node = stripParenExpression(wrappedNode);
41595
42668
  if (isNodeOfType(node, "CallExpression")) {
41596
42669
  if (isNodeOfType(node.callee, "Identifier")) return {
41597
42670
  calleeName: node.callee.name,
@@ -42179,20 +43252,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
42179
43252
  };
42180
43253
  const isReactNativeDimensionsCallee = (node, callee) => {
42181
43254
  if (!isNodeOfType(callee, "MemberExpression")) return false;
42182
- if (isNodeOfType(callee.object, "Identifier")) {
42183
- const localName = callee.object.name;
43255
+ const receiver = stripParenExpression(callee.object);
43256
+ if (isNodeOfType(receiver, "Identifier")) {
43257
+ const localName = receiver.name;
42184
43258
  const binding = findVariableInitializer(node, localName);
42185
43259
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
42186
43260
  return localName === "Dimensions";
42187
43261
  }
42188
- if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
42189
- if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
42190
- const rootName = getRootIdentifierName(callee.object.object);
43262
+ if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
43263
+ if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
43264
+ const rootName = getRootIdentifierName(receiver.object);
42191
43265
  if (rootName === null) return false;
42192
43266
  const importBinding = getImportBindingForName(node, rootName);
42193
43267
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
42194
43268
  }
42195
- if (getRequireCallSource(callee.object) === REACT_NATIVE_MODULE) return isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions";
43269
+ if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42196
43270
  return false;
42197
43271
  };
42198
43272
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -42636,7 +43710,8 @@ const rnNoLegacyShadowStyles = defineRule({
42636
43710
  },
42637
43711
  CallExpression(node) {
42638
43712
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
42639
- if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
43713
+ const receiver = stripParenExpression(node.callee.object);
43714
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42640
43715
  if (!isMemberProperty(node.callee, "create")) return;
42641
43716
  const stylesArgument = node.arguments?.[0];
42642
43717
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -43486,7 +44561,7 @@ const rnNoSetNativeProps = defineRule({
43486
44561
  const callee = node.callee;
43487
44562
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
43488
44563
  if (!isNodeOfType(callee, "MemberExpression")) return;
43489
- if (!isStaticMemberNamed(callee.object, "current")) return;
44564
+ if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43490
44565
  context.report({
43491
44566
  node,
43492
44567
  message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
@@ -43714,14 +44789,15 @@ const analyzeGestureChain = (expression) => {
43714
44789
  if (!isNodeOfType(callee, "MemberExpression")) return null;
43715
44790
  if (!isNodeOfType(callee.property, "Identifier")) return null;
43716
44791
  const methodName = callee.property.name;
43717
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
44792
+ const receiver = stripParenExpression(callee.object);
44793
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43718
44794
  factoryName: methodName,
43719
44795
  chainMethodNames,
43720
44796
  numberOfTapsArgument
43721
44797
  };
43722
44798
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
43723
44799
  chainMethodNames.push(methodName);
43724
- cursor = callee.object;
44800
+ cursor = receiver;
43725
44801
  }
43726
44802
  return null;
43727
44803
  };
@@ -47387,6 +48463,8 @@ const roleSupportsAriaProps = defineRule({
47387
48463
  const propName = propRawName.toLowerCase();
47388
48464
  if (!propName.startsWith("aria-")) continue;
47389
48465
  if (!ARIA_PROPERTIES.has(propName)) continue;
48466
+ const attributeValue = attributeNode.value;
48467
+ if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
47390
48468
  (ariaAttributes ??= []).push({
47391
48469
  attribute,
47392
48470
  propName
@@ -47756,21 +48834,6 @@ const isInsideLoop = (descendant, ancestor) => {
47756
48834
  }
47757
48835
  return false;
47758
48836
  };
47759
- const isUseEffectEventSymbol = (symbol) => {
47760
- const initializer = symbol.initializer;
47761
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47762
- return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47763
- };
47764
- const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47765
- const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47766
- return Boolean(symbol && symbol.kind !== "import");
47767
- };
47768
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
47769
- const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47770
- const initializer = symbol.initializer;
47771
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47772
- return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47773
- };
47774
48837
  const findEnclosingComponentOrHookFunction = (node) => {
47775
48838
  let current = node.parent;
47776
48839
  while (current) {
@@ -47960,8 +49023,7 @@ const rulesOfHooks = defineRule({
47960
49023
  },
47961
49024
  Identifier(node) {
47962
49025
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47963
- if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47964
- if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
49026
+ if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
47965
49027
  if (!isSameComponentOrHookScope(symbol, node)) return;
47966
49028
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47967
49029
  context.report({
@@ -48109,7 +49171,8 @@ const serverAfterNonblocking = defineRule({
48109
49171
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48110
49172
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48111
49173
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48112
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
49174
+ const receiver = stripParenExpression(node.callee.object);
49175
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48113
49176
  if (!objectName) return;
48114
49177
  const methodName = node.callee.property.name;
48115
49178
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -48785,7 +49848,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
48785
49848
  };
48786
49849
  const ascendMemberChain = (referenceIdentifier) => {
48787
49850
  let chainTip = referenceIdentifier;
48788
- while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
49851
+ while (chainTip.parent) {
49852
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
49853
+ chainTip = chainTip.parent;
49854
+ continue;
49855
+ }
49856
+ if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
49857
+ chainTip = chainTip.parent;
49858
+ continue;
49859
+ }
49860
+ break;
49861
+ }
48789
49862
  return chainTip;
48790
49863
  };
48791
49864
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -48810,7 +49883,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
48810
49883
  const callee = callExpression.callee;
48811
49884
  if (isNodeOfType(callee, "MemberExpression")) {
48812
49885
  const methodName = getMemberPropertyName$1(callee);
48813
- return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
49886
+ const calleeReceiver = stripParenExpression(callee.object);
49887
+ return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
48814
49888
  }
48815
49889
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
48816
49890
  const calleeSymbol = scopes.symbolFor(callee);
@@ -49540,7 +50614,7 @@ const walkServerFnChain = (outerNode) => {
49540
50614
  };
49541
50615
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
49542
50616
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
49543
- let currentNode = outerNode.callee.object;
50617
+ let currentNode = stripParenExpression(outerNode.callee.object);
49544
50618
  while (isNodeOfType(currentNode, "CallExpression")) {
49545
50619
  const calleeName = getCalleeName$2(currentNode);
49546
50620
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -49551,7 +50625,7 @@ const walkServerFnChain = (outerNode) => {
49551
50625
  }
49552
50626
  }
49553
50627
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
49554
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
50628
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49555
50629
  else break;
49556
50630
  }
49557
50631
  return result;
@@ -50204,7 +51278,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
50204
51278
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
50205
51279
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
50206
51280
  if (methodName) methodNames.unshift(methodName);
50207
- currentNode = currentNode.callee.object;
51281
+ currentNode = stripParenExpression(currentNode.callee.object);
50208
51282
  }
50209
51283
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
50210
51284
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -53286,6 +54360,18 @@ const reactDoctorRules = [
53286
54360
  category: "Bugs"
53287
54361
  }
53288
54362
  },
54363
+ {
54364
+ key: "react-doctor/no-locale-format-in-render",
54365
+ id: "no-locale-format-in-render",
54366
+ source: "react-doctor",
54367
+ originallyExternal: false,
54368
+ rule: {
54369
+ ...noLocaleFormatInRender,
54370
+ framework: "global",
54371
+ category: "Bugs",
54372
+ requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
54373
+ }
54374
+ },
53289
54375
  {
53290
54376
  key: "react-doctor/no-long-transition-duration",
53291
54377
  id: "no-long-transition-duration",
@@ -53714,6 +54800,18 @@ const reactDoctorRules = [
53714
54800
  category: "Maintainability"
53715
54801
  }
53716
54802
  },
54803
+ {
54804
+ key: "react-doctor/no-stale-timer-ref",
54805
+ id: "no-stale-timer-ref",
54806
+ source: "react-doctor",
54807
+ originallyExternal: false,
54808
+ rule: {
54809
+ ...noStaleTimerRef,
54810
+ framework: "global",
54811
+ category: "Bugs",
54812
+ requires: [...new Set(["react", ...noStaleTimerRef.requires ?? []])]
54813
+ }
54814
+ },
53717
54815
  {
53718
54816
  key: "react-doctor/no-static-element-interactions",
53719
54817
  id: "no-static-element-interactions",
@@ -56162,6 +57260,7 @@ const CROSS_FILE_RULE_IDS = new Set([
56162
57260
  "nextjs-no-use-search-params-without-suspense",
56163
57261
  "no-dynamic-import-path",
56164
57262
  "no-full-lodash-import",
57263
+ "no-locale-format-in-render",
56165
57264
  "no-mutating-reducer-state",
56166
57265
  "prefer-dynamic-import",
56167
57266
  "rendering-hydration-mismatch-time",
@@ -56296,6 +57395,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
56296
57395
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
56297
57396
  ["no-dynamic-import-path", collectNearestManifestDependencies],
56298
57397
  ["no-full-lodash-import", collectNearestManifestDependencies],
57398
+ ["no-locale-format-in-render", collectNearestManifestDependencies],
56299
57399
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
56300
57400
  ["prefer-dynamic-import", collectNearestManifestDependencies],
56301
57401
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],