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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +46 -0
  2. package/dist/index.js +1316 -429
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -532,6 +532,12 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
532
532
  ]);
533
533
  const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
534
534
  const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
535
+ const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
536
+ "WebSocket",
537
+ "EventSource",
538
+ "BroadcastChannel",
539
+ "RTCPeerConnection"
540
+ ]);
535
541
  const MUTABLE_GLOBAL_ROOTS = new Set([
536
542
  "location",
537
543
  "window",
@@ -687,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
687
693
  "unwatch",
688
694
  "unlisten",
689
695
  "unsub",
690
- "abort"
696
+ "abort",
697
+ "disconnect",
698
+ "unobserve",
699
+ "close"
691
700
  ]);
692
701
  const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
693
702
  "remove",
@@ -2268,6 +2277,7 @@ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2268
2277
  if (remainingHops === 0) return null;
2269
2278
  const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2270
2279
  if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2280
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
2271
2281
  return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
2272
2282
  }
2273
2283
  return null;
@@ -2307,7 +2317,8 @@ const isDirectChildOfLinkComponent = (openingElement) => {
2307
2317
  const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
2308
2318
  const isInvalidHref = (value, validHrefs) => {
2309
2319
  if (validHrefs.has(value)) return false;
2310
- return value === "" || value === "#" || value === "javascript:void(0)";
2320
+ const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
2321
+ return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
2311
2322
  };
2312
2323
  const isNullishOrFragmentHref = (value) => {
2313
2324
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -5182,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
5182
5193
  const callee = node.callee;
5183
5194
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
5184
5195
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
5185
- if (!isWebStorageObject(callee.object)) return;
5196
+ if (!isWebStorageObject(stripParenExpression(callee.object))) return;
5186
5197
  const keyArgument = node.arguments?.[0];
5187
5198
  if (!keyArgument) return;
5188
5199
  const keyString = resolveStaticKeyString(keyArgument);
@@ -6122,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
6122
6133
  })
6123
6134
  });
6124
6135
  //#endregion
6136
+ //#region src/plugin/utils/is-global-method-call.ts
6137
+ const isGlobalMethodCall = (node, objectName, methodName) => {
6138
+ if (!isNodeOfType(node, "CallExpression")) return false;
6139
+ const callee = stripParenExpression(node.callee);
6140
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
6141
+ const receiver = stripParenExpression(callee.object);
6142
+ return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
6143
+ };
6144
+ //#endregion
6125
6145
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6126
6146
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6127
6147
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6128
6148
  const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6129
6149
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6130
- const isJsonStringifyCall = (node) => {
6131
- if (!isNodeOfType(node, "CallExpression")) return false;
6132
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
6133
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
6134
- if (node.callee.object.name !== "JSON") return false;
6135
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
6136
- return node.callee.property.name === "stringify";
6137
- };
6150
+ const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6138
6151
  const resolveStringKey = (keyArg, context) => {
6139
6152
  if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
6140
6153
  if (!isNodeOfType(keyArg, "Identifier")) return null;
@@ -6153,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
6153
6166
  recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
6154
6167
  create: (context) => ({ CallExpression(node) {
6155
6168
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6156
- if (!isNodeOfType(node.callee.object, "Identifier")) return;
6157
- if (!STORAGE_OBJECTS.has(node.callee.object.name)) return;
6169
+ const receiver = stripParenExpression(node.callee.object);
6170
+ if (!isNodeOfType(receiver, "Identifier")) return;
6171
+ if (!STORAGE_OBJECTS.has(receiver.name)) return;
6158
6172
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6159
6173
  if (node.callee.property.name !== "setItem") return;
6160
6174
  const keyArg = node.arguments?.[0];
@@ -6167,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
6167
6181
  if (!isJsonStringifyCall(valueArg)) return;
6168
6182
  context.report({
6169
6183
  node: keyArg,
6170
- message: `${node.callee.object.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6184
+ message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6171
6185
  });
6172
6186
  } })
6173
6187
  });
@@ -7648,6 +7662,96 @@ const displayName = defineRule({
7648
7662
  }
7649
7663
  });
7650
7664
  //#endregion
7665
+ //#region src/plugin/utils/is-react-hook-name.ts
7666
+ const isReactHookName = (name) => {
7667
+ if (!name.startsWith("use")) return false;
7668
+ if (name.length === 3) return true;
7669
+ const fourthCharacter = name.charCodeAt(3);
7670
+ return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
7671
+ };
7672
+ //#endregion
7673
+ //#region src/plugin/utils/is-react-component-or-hook-name.ts
7674
+ const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
7675
+ //#endregion
7676
+ //#region src/plugin/utils/component-or-hook-display-name.ts
7677
+ const hocWrapperCalleeName = (callee) => {
7678
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
7679
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
7680
+ return null;
7681
+ };
7682
+ const displayNameFromFunctionBinding = (functionNode) => {
7683
+ let current = functionNode;
7684
+ for (;;) {
7685
+ const parent = current.parent;
7686
+ if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
7687
+ const calleeName = hocWrapperCalleeName(parent.callee);
7688
+ if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
7689
+ current = parent;
7690
+ continue;
7691
+ }
7692
+ }
7693
+ break;
7694
+ }
7695
+ const binding = current.parent;
7696
+ if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
7697
+ return null;
7698
+ };
7699
+ const componentOrHookDisplayNameForFunction = (functionNode) => {
7700
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
7701
+ return displayNameFromFunctionBinding(functionNode);
7702
+ };
7703
+ //#endregion
7704
+ //#region src/plugin/utils/find-enclosing-function.ts
7705
+ const findEnclosingFunction = (node) => {
7706
+ let cursor = node.parent;
7707
+ while (cursor) {
7708
+ if (isFunctionLike$1(cursor)) return cursor;
7709
+ cursor = cursor.parent ?? null;
7710
+ }
7711
+ return null;
7712
+ };
7713
+ //#endregion
7714
+ //#region src/plugin/utils/enclosing-component-or-hook-name.ts
7715
+ const enclosingComponentOrHookName = (node) => {
7716
+ const functionNode = findEnclosingFunction(node);
7717
+ return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
7718
+ };
7719
+ //#endregion
7720
+ //#region src/plugin/utils/is-result-discarded-call.ts
7721
+ const isResultDiscardedCall = (callExpression) => {
7722
+ let node = callExpression;
7723
+ let parent = node.parent;
7724
+ while (parent) {
7725
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
7726
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
7727
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
7728
+ if (isNodeOfType(parent, "ChainExpression")) {
7729
+ node = parent;
7730
+ parent = node.parent;
7731
+ continue;
7732
+ }
7733
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
7734
+ node = parent;
7735
+ parent = node.parent;
7736
+ continue;
7737
+ }
7738
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
7739
+ node = parent;
7740
+ parent = node.parent;
7741
+ continue;
7742
+ }
7743
+ if (isNodeOfType(parent, "SequenceExpression")) {
7744
+ const expressions = parent.expressions ?? [];
7745
+ if (expressions[expressions.length - 1] !== node) return true;
7746
+ node = parent;
7747
+ parent = node.parent;
7748
+ continue;
7749
+ }
7750
+ return false;
7751
+ }
7752
+ return false;
7753
+ };
7754
+ //#endregion
7651
7755
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7652
7756
  const walkInsideStatementBlocks = (node, visitor) => {
7653
7757
  if (!node || typeof node !== "object") return;
@@ -7799,6 +7903,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7799
7903
  };
7800
7904
  //#endregion
7801
7905
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
7906
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
7907
+ const RESOURCE_NOUN_BY_KIND = {
7908
+ subscribe: "subscription",
7909
+ timer: "timer",
7910
+ socket: "connection"
7911
+ };
7912
+ const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
7913
+ const isSubscribeOrObserveCall = (node) => {
7914
+ if (isSubscribeLikeCallExpression(node)) return true;
7915
+ return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
7916
+ };
7802
7917
  const findSubscribeLikeUsages = (callback) => {
7803
7918
  const usages = [];
7804
7919
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7810,6 +7925,14 @@ const findSubscribeLikeUsages = (callback) => {
7810
7925
  }
7811
7926
  walkAst(callback, (child) => {
7812
7927
  if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
7928
+ if (isSocketConstruction(child)) {
7929
+ usages.push({
7930
+ kind: "socket",
7931
+ node: child,
7932
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
7933
+ });
7934
+ return;
7935
+ }
7813
7936
  if (!isNodeOfType(child, "CallExpression")) return;
7814
7937
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7815
7938
  usages.push({
@@ -7819,7 +7942,7 @@ const findSubscribeLikeUsages = (callback) => {
7819
7942
  });
7820
7943
  return;
7821
7944
  }
7822
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
7945
+ if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
7823
7946
  kind: "subscribe",
7824
7947
  node: child,
7825
7948
  resourceName: child.callee.property.name
@@ -7835,6 +7958,13 @@ const collectCleanupBindings = (effectCallback) => {
7835
7958
  };
7836
7959
  if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
7837
7960
  if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
7961
+ walkAst(effectCallback.body, (child) => {
7962
+ if (!isSubscribeOrObserveCall(child)) return;
7963
+ if (!isNodeOfType(child, "CallExpression")) return;
7964
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
7965
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
7966
+ bindings.subscriptionNames.add(child.callee.object.name);
7967
+ });
7838
7968
  walkInsideStatementBlocks(effectCallback.body, (child) => {
7839
7969
  if (!isNodeOfType(child, "VariableDeclaration")) return;
7840
7970
  for (const declarator of child.declarations ?? []) {
@@ -7842,7 +7972,12 @@ const collectCleanupBindings = (effectCallback) => {
7842
7972
  const bindingName = declarator.id.name;
7843
7973
  bindings.effectScopeVariableNames.add(bindingName);
7844
7974
  const init = declarator.init;
7845
- if (!init || !isNodeOfType(init, "CallExpression")) continue;
7975
+ if (!init) continue;
7976
+ if (isSocketConstruction(init)) {
7977
+ bindings.subscriptionNames.add(bindingName);
7978
+ continue;
7979
+ }
7980
+ if (!isNodeOfType(init, "CallExpression")) continue;
7846
7981
  if (isSubscribeLikeCallExpression(init)) {
7847
7982
  bindings.subscriptionNames.add(bindingName);
7848
7983
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -7872,6 +8007,22 @@ const getRangeStart = (node) => {
7872
8007
  const rangeStart = node.range?.[0];
7873
8008
  return typeof rangeStart === "number" ? rangeStart : null;
7874
8009
  };
8010
+ const removeSynchronouslyReleasedUsages = (callback, usages) => {
8011
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8012
+ if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8013
+ const releaseStarts = [];
8014
+ walkInsideStatementBlocks(callback.body, (child) => {
8015
+ if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8016
+ const releaseStart = getRangeStart(child);
8017
+ if (releaseStart !== null) releaseStarts.push(releaseStart);
8018
+ });
8019
+ if (releaseStarts.length === 0) return usages;
8020
+ return usages.filter((usage) => {
8021
+ const usageStart = getRangeStart(usage.node);
8022
+ if (usageStart === null) return true;
8023
+ return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8024
+ });
8025
+ };
7875
8026
  const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
7876
8027
  if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
7877
8028
  const returnStart = getRangeStart(returnStatement);
@@ -7894,26 +8045,177 @@ const effectHasCleanupReturn = (callback, usages) => {
7894
8045
  });
7895
8046
  return didFindCleanupReturn;
7896
8047
  };
8048
+ const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8049
+ const isSelfReleasingListenerOptionProperty = (property) => {
8050
+ if (!isNodeOfType(property, "Property")) return false;
8051
+ const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
8052
+ if (keyName === "signal") return true;
8053
+ if (keyName !== "once") return false;
8054
+ return isNodeOfType(property.value, "Literal") && property.value.value === true;
8055
+ };
8056
+ const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
8057
+ const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8058
+ ["addEventListener", new Set(["removeEventListener", "abort"])],
8059
+ ["addListener", new Set([
8060
+ "removeListener",
8061
+ "off",
8062
+ "abort"
8063
+ ])],
8064
+ ["on", new Set([
8065
+ "off",
8066
+ "removeListener",
8067
+ "on"
8068
+ ])],
8069
+ ["subscribe", new Set(["unsubscribe", "unsub"])],
8070
+ ["sub", new Set(["unsub", "unsubscribe"])],
8071
+ ["watch", new Set(["unwatch", "close"])],
8072
+ ["listen", new Set(["unlisten", "close"])],
8073
+ [OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
8074
+ ]);
8075
+ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8076
+ "cleanup",
8077
+ "dispose",
8078
+ "destroy",
8079
+ "teardown"
8080
+ ]);
8081
+ const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8082
+ const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8083
+ const getReleaseVerbName = (node) => {
8084
+ if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8085
+ const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8086
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8087
+ const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8088
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
8089
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
8090
+ return null;
8091
+ };
8092
+ const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8093
+ const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8094
+ let didFindPairedRelease = false;
8095
+ walkAst(body, (child) => {
8096
+ if (didFindPairedRelease) return false;
8097
+ if (child !== body && isFunctionLike$1(child)) return false;
8098
+ const releaseVerbName = getReleaseVerbName(child);
8099
+ if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8100
+ didFindPairedRelease = true;
8101
+ return false;
8102
+ }
8103
+ });
8104
+ return didFindPairedRelease;
8105
+ };
8106
+ const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8107
+ const collectFileReleaseVerbNames = (anyNode) => {
8108
+ let programNode = anyNode;
8109
+ while (programNode.parent) programNode = programNode.parent;
8110
+ const cached = fileReleaseVerbNamesCache.get(programNode);
8111
+ if (cached) return cached;
8112
+ const releaseVerbNames = /* @__PURE__ */ new Set();
8113
+ walkAst(programNode, (child) => {
8114
+ const releaseVerbName = getReleaseVerbName(child);
8115
+ if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
8116
+ });
8117
+ fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8118
+ return releaseVerbNames;
8119
+ };
8120
+ const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8121
+ const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8122
+ const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8123
+ if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8124
+ for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
8125
+ return false;
8126
+ };
8127
+ const findRetainedFunctionLeak = (retainedFunction) => {
8128
+ if (!isFunctionLike$1(retainedFunction)) return null;
8129
+ const body = retainedFunction.body;
8130
+ if (!body) return null;
8131
+ const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8132
+ let leak = null;
8133
+ walkAst(body, (child) => {
8134
+ if (leak !== null) return false;
8135
+ if (isFunctionLike$1(child)) return false;
8136
+ if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8137
+ if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8138
+ leak = {
8139
+ kind: "socket",
8140
+ node: child,
8141
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8142
+ };
8143
+ return false;
8144
+ }
8145
+ if (!isNodeOfType(child, "CallExpression")) return;
8146
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8147
+ leak = {
8148
+ kind: "timer",
8149
+ node: child,
8150
+ resourceName: "setInterval"
8151
+ };
8152
+ return false;
8153
+ }
8154
+ if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8155
+ const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8156
+ if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
8157
+ kind: "subscribe",
8158
+ node: child,
8159
+ resourceName: registrationVerbName
8160
+ };
8161
+ return false;
8162
+ }
8163
+ });
8164
+ return leak;
8165
+ };
8166
+ const isRetainedComponentScopeFunction = (functionNode) => {
8167
+ if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
8168
+ if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
8169
+ if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8170
+ return enclosingComponentOrHookName(functionNode) !== null;
8171
+ };
7897
8172
  const effectNeedsCleanup = defineRule({
7898
8173
  id: "effect-needs-cleanup",
7899
8174
  title: "Effect subscription or timer never cleaned up",
7900
8175
  severity: "error",
7901
8176
  tags: ["test-noise"],
7902
- recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
7903
- create: (context) => ({ CallExpression(node) {
7904
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
7905
- const callback = getEffectCallback(node);
7906
- if (!callback) return;
7907
- const usages = findSubscribeLikeUsages(callback);
7908
- if (usages.length === 0) return;
7909
- if (effectHasCleanupReturn(callback, usages)) return;
7910
- const firstUsage = usages[0];
7911
- const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
7912
- context.report({
7913
- node,
7914
- message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
7915
- });
7916
- } })
8177
+ recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
8178
+ create: (context) => {
8179
+ const reportRetainedLeak = (retainedFunction) => {
8180
+ const leak = findRetainedFunctionLeak(retainedFunction);
8181
+ if (!leak) return;
8182
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8183
+ context.report({
8184
+ node: leak.node,
8185
+ message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
8186
+ });
8187
+ };
8188
+ return {
8189
+ CallExpression(node) {
8190
+ if (isHookCall$2(node, "useCallback")) {
8191
+ const retainedCallback = getEffectCallback(node);
8192
+ if (retainedCallback) reportRetainedLeak(retainedCallback);
8193
+ return;
8194
+ }
8195
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
8196
+ const callback = getEffectCallback(node);
8197
+ if (!callback) return;
8198
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
8199
+ if (usages.length === 0) return;
8200
+ if (effectHasCleanupReturn(callback, usages)) return;
8201
+ const firstUsage = usages[0];
8202
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
8203
+ context.report({
8204
+ node,
8205
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8206
+ });
8207
+ },
8208
+ FunctionDeclaration(node) {
8209
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8210
+ },
8211
+ ArrowFunctionExpression(node) {
8212
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8213
+ },
8214
+ FunctionExpression(node) {
8215
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8216
+ }
8217
+ };
8218
+ }
7917
8219
  });
7918
8220
  //#endregion
7919
8221
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8472,17 +8774,6 @@ const closureCaptures = (functionNode, scopes) => {
8472
8774
  return computedCaptures;
8473
8775
  };
8474
8776
  //#endregion
8475
- //#region src/plugin/utils/is-react-hook-name.ts
8476
- const isReactHookName = (name) => {
8477
- if (!name.startsWith("use")) return false;
8478
- if (name.length === 3) return true;
8479
- const fourthCharacter = name.charCodeAt(3);
8480
- return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8481
- };
8482
- //#endregion
8483
- //#region src/plugin/utils/is-react-component-or-hook-name.ts
8484
- const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8485
- //#endregion
8486
8777
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8487
8778
  const reactHocCalleeName = (callee) => {
8488
8779
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -8696,8 +8987,8 @@ const isAstDescendant = (inner, outer) => {
8696
8987
  * One cohesive concept: "given a captured symbol, is its value
8697
8988
  * structurally stable across re-renders (and therefore unnecessary
8698
8989
  * in a deps array)?". The rule reads `symbolHasStableValue` /
8699
- * `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
8700
- * `isRecursiveInitializerCapture` at multiple sites — extracting
8990
+ * `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
8991
+ * multiple sites — extracting
8701
8992
  * them lets the rule body stay focused on the diff-the-captured-vs-
8702
8993
  * declared logic.
8703
8994
  *
@@ -8753,11 +9044,6 @@ const symbolHasStableHookOrigin = (symbol) => {
8753
9044
  }
8754
9045
  return false;
8755
9046
  };
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
9047
  const getFunctionValueNode = (symbol) => {
8762
9048
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
8763
9049
  const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
@@ -8843,6 +9129,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
8843
9129
  };
8844
9130
  const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
8845
9131
  //#endregion
9132
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
9133
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
9134
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
9135
+ if (importSource === null) return false;
9136
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
9137
+ };
9138
+ //#endregion
9139
+ //#region src/plugin/utils/is-non-react-effect-event-callee.ts
9140
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
9141
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
9142
+ return Boolean(symbol && symbol.kind !== "import");
9143
+ };
9144
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
9145
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
9146
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
9147
+ return false;
9148
+ };
9149
+ //#endregion
9150
+ //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9151
+ const getUseEffectEventCalleeName = (callee) => {
9152
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
9153
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9154
+ return null;
9155
+ };
9156
+ const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
9157
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
9158
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9159
+ if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
9160
+ return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9161
+ };
9162
+ //#endregion
8846
9163
  //#region src/plugin/rules/react-builtins/exhaustive-deps.ts
8847
9164
  const HOOKS_REQUIRING_DEPS_MATCH = new Set([
8848
9165
  "useEffect",
@@ -9493,7 +9810,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9493
9810
  if (isLiteralOrEmptyTemplate(stripped)) continue;
9494
9811
  if (isNodeOfType(stripped, "Identifier")) {
9495
9812
  const depSymbol = context.scopes.symbolFor(stripped);
9496
- if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
9813
+ if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
9497
9814
  context.report({
9498
9815
  node: elementNode,
9499
9816
  message: buildEffectEventDepMessage()
@@ -9919,7 +10236,10 @@ const PRAGMA = "React";
9919
10236
  const isReactFunctionCall = (node, expectedCall) => {
9920
10237
  if (!isNodeOfType(node, "CallExpression")) return false;
9921
10238
  if (getCalleeName$2(node) !== expectedCall) return false;
9922
- if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10239
+ if (isNodeOfType(node.callee, "MemberExpression")) {
10240
+ const receiver = stripParenExpression(node.callee.object);
10241
+ return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10242
+ }
9923
10243
  return true;
9924
10244
  };
9925
10245
  //#endregion
@@ -11297,16 +11617,6 @@ const collectHandlerReferencedNames = (root) => {
11297
11617
  return names;
11298
11618
  };
11299
11619
  //#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
11620
  //#region src/plugin/utils/get-function-binding-name.ts
11311
11621
  const getFunctionBindingIdentifier = (functionNode) => {
11312
11622
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -12006,14 +12316,15 @@ const jsCacheStorage = defineRule({
12006
12316
  "ArrowFunctionExpression:exit": exitFunctionScope,
12007
12317
  CallExpression(node) {
12008
12318
  if (!isMemberProperty(node.callee, "getItem")) return;
12009
- if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
12319
+ const receiver = stripParenExpression(node.callee.object);
12320
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
12010
12321
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
12011
12322
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
12012
12323
  const storageKey = String(node.arguments[0].value);
12013
12324
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
12014
12325
  storageReadCounts.set(storageKey, readCount);
12015
12326
  if (readCount === 2) {
12016
- const storageName = node.callee.object.name;
12327
+ const storageName = receiver.name;
12017
12328
  context.report({
12018
12329
  node,
12019
12330
  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 +12338,16 @@ const jsCacheStorage = defineRule({
12027
12338
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
12028
12339
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
12029
12340
  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;
12341
+ if (isNodeOfType(callee, "MemberExpression")) {
12342
+ const receiver = stripParenExpression(callee.object);
12343
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12344
+ if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12345
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12346
+ return true;
12347
+ }
12348
+ return false;
12036
12349
  }
12350
+ if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12037
12351
  return false;
12038
12352
  };
12039
12353
  const isChainPassThroughCall = (callExpression) => {
@@ -12045,10 +12359,7 @@ const isChainPassThroughCall = (callExpression) => {
12045
12359
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12046
12360
  let cursor = receiverNode;
12047
12361
  while (cursor) {
12048
- if (isNodeOfType(cursor, "ChainExpression")) {
12049
- cursor = cursor.expression;
12050
- continue;
12051
- }
12362
+ cursor = stripParenExpression(cursor);
12052
12363
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12053
12364
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12054
12365
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12124,10 +12435,7 @@ const isStringSplitRootedChain = (receiverNode) => {
12124
12435
  let hops = 0;
12125
12436
  while (cursor && hops < 12) {
12126
12437
  hops += 1;
12127
- if (isNodeOfType(cursor, "ChainExpression")) {
12128
- cursor = cursor.expression;
12129
- continue;
12130
- }
12438
+ cursor = stripParenExpression(cursor);
12131
12439
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12132
12440
  const callee = cursor.callee;
12133
12441
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12151,10 +12459,7 @@ const isSmallLiteralArray = (node) => {
12151
12459
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12152
12460
  let cursor = receiverNode;
12153
12461
  while (cursor) {
12154
- if (isNodeOfType(cursor, "ChainExpression")) {
12155
- cursor = cursor.expression;
12156
- continue;
12157
- }
12462
+ cursor = stripParenExpression(cursor);
12158
12463
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12159
12464
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12160
12465
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12219,7 +12524,7 @@ const jsCombineIterations = defineRule({
12219
12524
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12220
12525
  const outerMethod = node.callee.property.name;
12221
12526
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12222
- const innerCall = node.callee.object;
12527
+ const innerCall = stripParenExpression(node.callee.object);
12223
12528
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12224
12529
  const innerMethod = innerCall.callee.property.name;
12225
12530
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12292,10 +12597,10 @@ const jsFlatmapFilter = defineRule({
12292
12597
  if (!filterArgument) return;
12293
12598
  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
12599
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12295
- const innerCall = node.callee.object;
12600
+ const innerCall = stripParenExpression(node.callee.object);
12296
12601
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12297
12602
  if (innerCall.callee.property.name !== "map") return;
12298
- const receiver = innerCall.callee.object;
12603
+ const receiver = stripParenExpression(innerCall.callee.object);
12299
12604
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12300
12605
  const elements = receiver.elements ?? [];
12301
12606
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13555,7 +13860,7 @@ const jsTosortedImmutable = defineRule({
13555
13860
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13556
13861
  create: (context) => ({ CallExpression(node) {
13557
13862
  if (!isMemberProperty(node.callee, "sort")) return;
13558
- const receiver = node.callee.object;
13863
+ const receiver = stripParenExpression(node.callee.object);
13559
13864
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13560
13865
  const spreadArgument = receiver.elements[0].argument;
13561
13866
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18592,7 +18897,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18592
18897
  };
18593
18898
  const describeClientSideNavigation = (node) => {
18594
18899
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18595
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
18900
+ const receiver = stripParenExpression(node.callee.object);
18901
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18596
18902
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18597
18903
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18598
18904
  }
@@ -19212,7 +19518,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19212
19518
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19213
19519
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19214
19520
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19215
- if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
19521
+ if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19216
19522
  return node.callee.property.name;
19217
19523
  };
19218
19524
  const isMutatingFetchCall = (node) => {
@@ -19226,7 +19532,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19226
19532
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19227
19533
  const { property, object } = node.callee;
19228
19534
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19229
- if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
19535
+ if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19230
19536
  return true;
19231
19537
  };
19232
19538
  const getDbCallDescription = (node) => {
@@ -19234,7 +19540,8 @@ const getDbCallDescription = (node) => {
19234
19540
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19235
19541
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19236
19542
  const methodName = node.callee.property.name;
19237
- const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19543
+ const receiver = stripParenExpression(node.callee.object);
19544
+ const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19238
19545
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19239
19546
  };
19240
19547
  const findSideEffect = (node, options = {}) => {
@@ -20634,13 +20941,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20634
20941
  if (isNodeOfType(current, "CallExpression")) {
20635
20942
  let node = ref.identifier;
20636
20943
  let parent = node.parent;
20637
- while (parent && isNodeOfType(parent, "MemberExpression")) {
20944
+ while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20638
20945
  node = parent;
20639
20946
  parent = node.parent;
20640
20947
  }
20641
20948
  if (current.callee === node) return current;
20642
20949
  }
20643
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
20950
+ if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20644
20951
  return null;
20645
20952
  };
20646
20953
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -20775,7 +21082,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
20775
21082
  const importDeclaration = declarationNode.parent;
20776
21083
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
20777
21084
  }));
20778
- const isHookCallee = (analysis, node, hookName) => {
21085
+ const isHookCallee$1 = (analysis, node, hookName) => {
20779
21086
  if (!node) return false;
20780
21087
  if (isNodeOfType(node, "Identifier")) {
20781
21088
  if (node.name === hookName) return true;
@@ -20784,15 +21091,19 @@ const isHookCallee = (analysis, node, hookName) => {
20784
21091
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
20785
21092
  return false;
20786
21093
  }
20787
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21094
+ if (isNodeOfType(node, "MemberExpression")) {
21095
+ const receiver = stripParenExpression(node.object);
21096
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21097
+ }
20788
21098
  return false;
20789
21099
  };
20790
21100
  const isUseEffect = (node) => {
20791
21101
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
20792
21102
  const callee = node.callee;
20793
21103
  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;
21104
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
21105
+ const receiver = stripParenExpression(callee.object);
21106
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
20796
21107
  };
20797
21108
  const getEffectFn = (analysis, node) => {
20798
21109
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -20820,7 +21131,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20820
21131
  const node = def.node;
20821
21132
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20822
21133
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20823
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21134
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20824
21135
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20825
21136
  const elements = node.id.elements ?? [];
20826
21137
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -20831,7 +21142,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
20831
21142
  const node = def.node;
20832
21143
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20833
21144
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20834
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21145
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20835
21146
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20836
21147
  const elements = node.id.elements ?? [];
20837
21148
  if (elements.length !== 2) return false;
@@ -20878,7 +21189,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20878
21189
  const node = def.node;
20879
21190
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20880
21191
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20881
- return isHookCallee(analysis, node.init.callee, "useRef");
21192
+ return isHookCallee$1(analysis, node.init.callee, "useRef");
20882
21193
  }));
20883
21194
  const isRefCurrent = (ref) => {
20884
21195
  const parent = ref.identifier.parent;
@@ -20891,11 +21202,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
20891
21202
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
20892
21203
  const isPropCallbackInvocationRef = (analysis, ref) => {
20893
21204
  if (!isPropAlias(analysis, ref)) return false;
20894
- const identifier = ref.identifier;
20895
- const parent = identifier.parent;
21205
+ let effectiveNode = ref.identifier;
21206
+ let parent = effectiveNode.parent;
21207
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21208
+ effectiveNode = parent;
21209
+ parent = effectiveNode.parent;
21210
+ }
20896
21211
  if (!parent) return false;
20897
- if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
20898
- if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21212
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21213
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
20899
21214
  const memberParent = parent.parent;
20900
21215
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
20901
21216
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -20906,7 +21221,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
20906
21221
  };
20907
21222
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
20908
21223
  const getUseStateDecl = (analysis, ref) => {
20909
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21224
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
20910
21225
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
20911
21226
  return node ?? null;
20912
21227
  };
@@ -21111,7 +21426,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21111
21426
  const noAdjustStateOnPropChange = defineRule({
21112
21427
  id: "no-adjust-state-on-prop-change",
21113
21428
  title: "State synced to a prop inside an effect",
21114
- severity: "error",
21429
+ severity: "warn",
21115
21430
  tags: ["test-noise"],
21116
21431
  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
21432
  create: (context) => ({ CallExpression(node) {
@@ -21152,7 +21467,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21152
21467
  "summary",
21153
21468
  "textarea"
21154
21469
  ]);
21470
+ const isStaticallyFalseBooleanAttribute = (attribute) => {
21471
+ const value = attribute.value;
21472
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
21473
+ const expression = value.expression;
21474
+ return isNodeOfType(expression, "Literal") && expression.value === false;
21475
+ };
21476
+ const DISABLEABLE_TAGS = new Set([
21477
+ "button",
21478
+ "input",
21479
+ "select",
21480
+ "textarea"
21481
+ ]);
21155
21482
  const isNativelyFocusable = (tagName, openingElement) => {
21483
+ if (DISABLEABLE_TAGS.has(tagName)) {
21484
+ const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
21485
+ if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
21486
+ }
21156
21487
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21157
21488
  switch (tagName) {
21158
21489
  case "input": {
@@ -21166,7 +21497,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
21166
21497
  case "a":
21167
21498
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21168
21499
  case "audio":
21169
- case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
21500
+ case "video": {
21501
+ const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
21502
+ return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
21503
+ }
21170
21504
  default: return false;
21171
21505
  }
21172
21506
  };
@@ -22130,7 +22464,8 @@ const SECOND_INDEX_METHODS = new Set([
22130
22464
  "some"
22131
22465
  ]);
22132
22466
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22133
- const isPositionallyStableIterationReceiver = (receiver) => {
22467
+ const isPositionallyStableIterationReceiver = (receiverNode) => {
22468
+ const receiver = stripParenExpression(receiverNode);
22134
22469
  if (isAllLiteralArrayExpression(receiver)) return true;
22135
22470
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22136
22471
  const only = receiver.elements[0];
@@ -22141,17 +22476,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
22141
22476
  }
22142
22477
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22143
22478
  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;
22479
+ if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22145
22480
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22146
22481
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22147
22482
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22148
22483
  return false;
22149
22484
  };
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
- };
22485
+ const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22155
22486
  const isArrayFromSourcePositionallyStable = (source) => {
22156
22487
  if (isNodeOfType(source, "ObjectExpression")) {
22157
22488
  for (const property of source.properties ?? []) {
@@ -22210,18 +22541,12 @@ const expressionUsesIndex = (expression, paramName) => {
22210
22541
  return false;
22211
22542
  }
22212
22543
  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;
22544
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22214
22545
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22215
22546
  }
22216
22547
  return false;
22217
22548
  };
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
- };
22549
+ const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22225
22550
  const noArrayIndexKey = defineRule({
22226
22551
  id: "no-array-index-key",
22227
22552
  title: "Array index used as a key",
@@ -22945,7 +23270,7 @@ const isAsyncFunctionLike = (node) => {
22945
23270
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
22946
23271
  return false;
22947
23272
  };
22948
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23273
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
22949
23274
  "forEach",
22950
23275
  "map",
22951
23276
  "filter",
@@ -22966,30 +23291,51 @@ const runsOnEffectDispatch = (functionNode) => {
22966
23291
  if (parent.callee === functionNode) return true;
22967
23292
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
22968
23293
  const callee = parent.callee;
22969
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
23294
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
22970
23295
  };
22971
23296
  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) => {
23297
+ const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
23298
+ const analyzeStatementSequence = (statements, context) => {
22981
23299
  let fallThroughCount = 0;
22982
- let maxTerminatingPathCount = 0;
23300
+ let maxTerminatedCount = 0;
22983
23301
  for (const statement of statements) {
22984
- const guardBranch = isGuardWithTerminatingBranch(statement);
22985
- if (guardBranch) {
22986
- maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
23302
+ if (isNodeOfType(statement, "IfStatement")) {
23303
+ fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
23304
+ const thenSummary = analyzeBranchStatements(statement.consequent, context);
23305
+ const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
23306
+ fallThroughCount: 0,
23307
+ maxTerminatedCount: 0,
23308
+ doAllPathsTerminate: false
23309
+ };
23310
+ maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
23311
+ if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
23312
+ fallThroughCount: 0,
23313
+ maxTerminatedCount,
23314
+ doAllPathsTerminate: true
23315
+ };
23316
+ const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
23317
+ fallThroughCount += Math.max(...fallThroughBranchCounts);
22987
23318
  continue;
22988
23319
  }
22989
- if (isTerminatingStatement(statement)) break;
23320
+ if (isTerminatingStatement(statement)) {
23321
+ const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
23322
+ return {
23323
+ fallThroughCount: 0,
23324
+ maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
23325
+ doAllPathsTerminate: true
23326
+ };
23327
+ }
22990
23328
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
22991
23329
  }
22992
- return Math.max(maxTerminatingPathCount, fallThroughCount);
23330
+ return {
23331
+ fallThroughCount,
23332
+ maxTerminatedCount,
23333
+ doAllPathsTerminate: false
23334
+ };
23335
+ };
23336
+ const countStatementSequenceSetStateCalls = (statements, context) => {
23337
+ const summary = analyzeStatementSequence(statements, context);
23338
+ return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
22993
23339
  };
22994
23340
  const collectLocalHelperFunctions = (root) => {
22995
23341
  const helpersByName = /* @__PURE__ */ new Map();
@@ -23120,7 +23466,9 @@ const isDevOnlyGuardedEffect = (callback) => {
23120
23466
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23121
23467
  const firstStatement = (body.body ?? [])[0];
23122
23468
  if (!firstStatement) return false;
23123
- if (!isGuardWithTerminatingBranch(firstStatement)) return false;
23469
+ if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
23470
+ const consequent = firstStatement.consequent;
23471
+ if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23124
23472
  return mentionsDevEnvFlag(firstStatement.test);
23125
23473
  };
23126
23474
  const noCascadingSetState = defineRule({
@@ -23479,40 +23827,6 @@ const noCloneElement = defineRule({
23479
23827
  } })
23480
23828
  });
23481
23829
  //#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
23830
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23517
23831
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23518
23832
  const CONTEXT_MODULES = [
@@ -24483,11 +24797,21 @@ const isInitialOnlySetterCall = (callExpr) => {
24483
24797
  return isInitialOnlyPropName(arg.name);
24484
24798
  };
24485
24799
  //#endregion
24800
+ //#region src/plugin/utils/is-no-op-statement.ts
24801
+ const isNoOpStatement = (statement) => {
24802
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
24803
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24804
+ const expression = stripParenExpression(statement.expression);
24805
+ if (isNodeOfType(expression, "Literal")) return true;
24806
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
24807
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
24808
+ return false;
24809
+ };
24810
+ //#endregion
24486
24811
  //#region src/plugin/utils/get-callback-statements.ts
24487
24812
  const getCallbackStatements = (callback) => {
24488
24813
  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] : [];
24814
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24491
24815
  };
24492
24816
  //#endregion
24493
24817
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24526,6 +24850,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24526
24850
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24527
24851
  }
24528
24852
  };
24853
+ const flattenGuardedStatements = (statements) => {
24854
+ const flattened = [];
24855
+ for (const statement of statements) {
24856
+ if (isNoOpStatement(statement)) continue;
24857
+ if (isNodeOfType(statement, "ExpressionStatement")) {
24858
+ flattened.push(statement);
24859
+ continue;
24860
+ }
24861
+ if (isNodeOfType(statement, "IfStatement")) {
24862
+ for (const branch of [statement.consequent, statement.alternate]) {
24863
+ if (!branch) continue;
24864
+ const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
24865
+ if (flattenedBranch === null) return null;
24866
+ flattened.push(...flattenedBranch);
24867
+ }
24868
+ continue;
24869
+ }
24870
+ return null;
24871
+ }
24872
+ return flattened;
24873
+ };
24529
24874
  const noDerivedStateEffect = defineRule({
24530
24875
  id: "no-derived-state-effect",
24531
24876
  title: "Derived state stored in an effect",
@@ -24557,8 +24902,8 @@ const noDerivedStateEffect = defineRule({
24557
24902
  }
24558
24903
  }
24559
24904
  if (sawAnyDep && allDepsAreInitialOnly) return;
24560
- const statements = getCallbackStatements(callback);
24561
- if (statements.length === 0) return;
24905
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
24906
+ if (statements === null || statements.length === 0) return;
24562
24907
  if (!statements.every((statement) => {
24563
24908
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24564
24909
  const expression = statement.expression;
@@ -25193,7 +25538,7 @@ const noDidMountSetState = defineRule({
25193
25538
  const { mode } = resolveSettings$20(context.settings);
25194
25539
  return { CallExpression(node) {
25195
25540
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25196
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25541
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25197
25542
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25198
25543
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25199
25544
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25318,7 +25663,7 @@ const noDidUpdateSetState = defineRule({
25318
25663
  const { mode } = resolveSettings$19(context.settings);
25319
25664
  return { CallExpression(node) {
25320
25665
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25321
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25666
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25322
25667
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25323
25668
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25324
25669
  if (isInsideDiffGuard(node)) return;
@@ -25415,18 +25760,45 @@ const collectUseStateBindings = (componentBody) => {
25415
25760
  //#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
25416
25761
  const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
25417
25762
  const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
25763
+ const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
25764
+ const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
25765
+ "assign",
25766
+ "entries",
25767
+ "fromEntries",
25768
+ "keys",
25769
+ "values"
25770
+ ]);
25771
+ const ARRAY_COPY_METHOD_NAMES = new Set([
25772
+ "map",
25773
+ "filter",
25774
+ "slice",
25775
+ "concat",
25776
+ "flat",
25777
+ "flatMap",
25778
+ "toSorted",
25779
+ "toReversed",
25780
+ "toSpliced",
25781
+ "with"
25782
+ ]);
25418
25783
  const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
25419
25784
  const isPlainDataProducerCall = (expression) => {
25420
25785
  if (!isNodeOfType(expression, "CallExpression")) return false;
25421
25786
  const callee = expression.callee;
25422
25787
  if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
25423
25788
  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);
25789
+ if (isNodeOfType(callee.object, "Identifier")) {
25790
+ if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25791
+ if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
25792
+ if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
25793
+ }
25425
25794
  return producesPlainStateValue(callee.object);
25426
25795
  };
25796
+ const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
25797
+ const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
25427
25798
  const producesPlainStateValue = (expression) => {
25428
25799
  const unwrapped = stripParenExpression(expression);
25429
25800
  if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
25801
+ if (isPlainDataNewExpression(unwrapped)) return true;
25430
25802
  if (isNullOrUndefinedExpression(unwrapped)) return true;
25431
25803
  if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
25432
25804
  return isPlainDataProducerCall(unwrapped);
@@ -25441,6 +25813,38 @@ const initializerMarksPlainState = (initializerArgument) => {
25441
25813
  }
25442
25814
  return producesPlainStateValue(unwrapped);
25443
25815
  };
25816
+ const producesOpaqueInstanceValue = (expression) => {
25817
+ if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
25818
+ if (!isNodeOfType(expression, "CallExpression")) return false;
25819
+ const callee = expression.callee;
25820
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
25821
+ if (isPlainDataProducerCall(expression)) return false;
25822
+ if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
25823
+ return true;
25824
+ };
25825
+ const collectSetterValueObservations = (componentBody, setterNames) => {
25826
+ const plainFedSetterNames = /* @__PURE__ */ new Set();
25827
+ const opaqueFedSetterNames = /* @__PURE__ */ new Set();
25828
+ walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
25829
+ if (!isNodeOfType(node, "CallExpression")) return;
25830
+ if (!isNodeOfType(node.callee, "Identifier")) return;
25831
+ const setterName = node.callee.name;
25832
+ if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
25833
+ const argument = node.arguments?.[0];
25834
+ if (!argument) return;
25835
+ const unwrapped = stripParenExpression(argument);
25836
+ if (isNullOrUndefinedExpression(unwrapped)) return;
25837
+ if (producesPlainStateValue(unwrapped)) {
25838
+ plainFedSetterNames.add(setterName);
25839
+ return;
25840
+ }
25841
+ if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
25842
+ }, true);
25843
+ return {
25844
+ plainFedSetterNames,
25845
+ opaqueFedSetterNames
25846
+ };
25847
+ };
25444
25848
  const collectCallbackRefSetterNames = (componentBody) => {
25445
25849
  const callbackRefSetterNames = /* @__PURE__ */ new Set();
25446
25850
  walkAst(componentBody, (node) => {
@@ -25510,11 +25914,15 @@ const noDirectStateMutation = defineRule({
25510
25914
  if (bindings.length === 0) return;
25511
25915
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
25512
25916
  const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
25917
+ const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
25513
25918
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
25514
25919
  for (const binding of bindings) {
25515
25920
  if (callbackRefSetterNames.has(binding.setterName)) continue;
25516
25921
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
25517
- if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
25922
+ const initializerArgument = binding.declarator.init.arguments?.[0];
25923
+ if (!initializerMarksPlainState(initializerArgument)) continue;
25924
+ if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
25925
+ plainObjectStateValueNames.add(binding.valueName);
25518
25926
  }
25519
25927
  const visitMutationCandidate = (child, currentlyShadowed) => {
25520
25928
  if (isNodeOfType(child, "AssignmentExpression")) {
@@ -25639,9 +26047,10 @@ const noDocumentStartViewTransition = defineRule({
25639
26047
  create: (context) => ({ CallExpression(node) {
25640
26048
  const callee = node.callee;
25641
26049
  if (!isNodeOfType(callee, "MemberExpression")) return;
25642
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26050
+ const receiver = stripParenExpression(callee.object);
26051
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25643
26052
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
25644
- if (context.scopes.symbolFor(callee.object) !== null) return;
26053
+ if (context.scopes.symbolFor(receiver) !== null) return;
25645
26054
  if (!importsReactViewTransition(node)) return;
25646
26055
  context.report({
25647
26056
  node,
@@ -25661,7 +26070,8 @@ const noDocumentWrite = defineRule({
25661
26070
  create: (context) => ({ CallExpression(node) {
25662
26071
  const callee = node.callee;
25663
26072
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
25664
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26073
+ const receiver = stripParenExpression(callee.object);
26074
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25665
26075
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
25666
26076
  context.report({
25667
26077
  node,
@@ -26293,13 +26703,6 @@ const noEffectEventHandler = defineRule({
26293
26703
  }
26294
26704
  });
26295
26705
  //#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
26706
  //#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
26304
26707
  const createComponentBindingStackTracker = (callbacks) => {
26305
26708
  const componentBindingStack = [];
@@ -26353,11 +26756,7 @@ const noEffectEventInDeps = defineRule({
26353
26756
  const initializer = declaratorNode.init;
26354
26757
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26355
26758
  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
- }
26759
+ if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
26361
26760
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26362
26761
  } });
26363
26762
  return {
@@ -28937,7 +29336,7 @@ const noIsMounted = defineRule({
28937
29336
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
28938
29337
  create: (context) => ({ CallExpression(node) {
28939
29338
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
28940
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
29339
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
28941
29340
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
28942
29341
  if (!getParentComponent(node)) return;
28943
29342
  context.report({
@@ -28952,7 +29351,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
28952
29351
  const isJsonMethodCall = (node, method) => {
28953
29352
  if (!isNodeOfType(node, "CallExpression")) return false;
28954
29353
  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;
29354
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
29355
+ const receiver = stripParenExpression(callee.object);
29356
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
28956
29357
  };
28957
29358
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
28958
29359
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -29037,7 +29438,7 @@ const extractReturnTypeAnnotation = (returnType) => {
29037
29438
  const noJsxElementType = defineRule({
29038
29439
  id: "no-jsx-element-type",
29039
29440
  title: "No JSX.Element",
29040
- severity: "error",
29441
+ severity: "warn",
29041
29442
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
29042
29443
  create: (context) => {
29043
29444
  let isJsxImported = false;
@@ -29363,6 +29764,378 @@ const noLegacyContextApi = defineRule({
29363
29764
  }
29364
29765
  });
29365
29766
  //#endregion
29767
+ //#region src/plugin/utils/executes-during-render.ts
29768
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29769
+ "map",
29770
+ "filter",
29771
+ "forEach",
29772
+ "flatMap",
29773
+ "reduce",
29774
+ "reduceRight",
29775
+ "some",
29776
+ "every",
29777
+ "find",
29778
+ "findIndex",
29779
+ "findLast",
29780
+ "findLastIndex",
29781
+ "sort",
29782
+ "toSorted"
29783
+ ]);
29784
+ const executesDuringRender = (functionNode) => {
29785
+ const parent = functionNode.parent;
29786
+ if (!isNodeOfType(parent, "CallExpression")) return false;
29787
+ if (parent.callee === functionNode) return true;
29788
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
29789
+ 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;
29790
+ };
29791
+ //#endregion
29792
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29793
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
29794
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29795
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29796
+ return false;
29797
+ };
29798
+ //#endregion
29799
+ //#region src/plugin/utils/find-declarator-for-binding.ts
29800
+ const findDeclaratorForBinding = (bindingIdentifier) => {
29801
+ let ancestor = bindingIdentifier.parent;
29802
+ while (ancestor) {
29803
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29804
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29805
+ ancestor = ancestor.parent ?? null;
29806
+ }
29807
+ return null;
29808
+ };
29809
+ //#endregion
29810
+ //#region src/plugin/utils/references-falsy-initial-state.ts
29811
+ const isFalsyLiteral = (node) => {
29812
+ if (!node) return true;
29813
+ if (isNodeOfType(node, "Literal")) return !node.value;
29814
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
29815
+ };
29816
+ const isFalsyInitialStateBinding = (identifier) => {
29817
+ if (!isNodeOfType(identifier, "Identifier")) return false;
29818
+ const binding = findVariableInitializer(identifier, identifier.name);
29819
+ if (!binding) return false;
29820
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29821
+ if (!declarator?.init) return false;
29822
+ const init = stripParenExpression(declarator.init);
29823
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
29824
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
29825
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
29826
+ return isFalsyLiteral(init.arguments?.[0]);
29827
+ };
29828
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
29829
+ //#endregion
29830
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29831
+ const isGatedByFalsyInitialState = (node) => {
29832
+ let cursor = node;
29833
+ let parent = node.parent;
29834
+ while (parent) {
29835
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
29836
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29837
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29838
+ cursor = parent;
29839
+ parent = parent.parent ?? null;
29840
+ }
29841
+ return false;
29842
+ };
29843
+ //#endregion
29844
+ //#region src/plugin/utils/references-client-only-flag.ts
29845
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
29846
+ const referencesClientOnlyFlag = (expression) => {
29847
+ const unwrapped = stripParenExpression(expression);
29848
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
29849
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
29850
+ const property = unwrapped.property;
29851
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
29852
+ }
29853
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
29854
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
29855
+ return false;
29856
+ };
29857
+ //#endregion
29858
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
29859
+ const isInsideClientOnlyGuard = (node) => {
29860
+ let cursor = node;
29861
+ let parent = node.parent;
29862
+ while (parent) {
29863
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
29864
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
29865
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
29866
+ cursor = parent;
29867
+ parent = parent.parent ?? null;
29868
+ }
29869
+ return false;
29870
+ };
29871
+ //#endregion
29872
+ //#region src/plugin/rules/performance/no-locale-format-in-render.ts
29873
+ const LOCALE_FORMAT_METHOD_NAMES = new Set([
29874
+ "toLocaleString",
29875
+ "toLocaleDateString",
29876
+ "toLocaleTimeString"
29877
+ ]);
29878
+ const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
29879
+ const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
29880
+ const INTL_FORMAT_METHOD_NAMES = new Set([
29881
+ "format",
29882
+ "formatToParts",
29883
+ "formatRange"
29884
+ ]);
29885
+ const isProvableDateExpression = (expression) => {
29886
+ if (!expression) return false;
29887
+ const unwrapped = stripParenExpression(expression);
29888
+ return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
29889
+ };
29890
+ const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
29891
+ const receiverNameLooksDateFlavored = (expression) => {
29892
+ if (!expression) return false;
29893
+ const unwrapped = stripParenExpression(expression);
29894
+ if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
29895
+ if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
29896
+ if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
29897
+ return false;
29898
+ };
29899
+ const objectLiteralHasProperty = (objectExpression, propertyName) => {
29900
+ if (!objectExpression) return false;
29901
+ const unwrapped = stripParenExpression(objectExpression);
29902
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
29903
+ for (const property of unwrapped.properties ?? []) {
29904
+ if (!isNodeOfType(property, "Property")) continue;
29905
+ if (property.computed) continue;
29906
+ if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
29907
+ if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
29908
+ }
29909
+ return false;
29910
+ };
29911
+ const hasExplicitLocaleArgument = (argument) => {
29912
+ if (!argument) return false;
29913
+ const unwrapped = stripParenExpression(argument);
29914
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
29915
+ return true;
29916
+ };
29917
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
29918
+ const localeArgument = call.arguments?.[0];
29919
+ if (!hasExplicitLocaleArgument(localeArgument)) return false;
29920
+ const optionsArgument = call.arguments?.[1];
29921
+ if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
29922
+ return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
29923
+ };
29924
+ const matchLocaleMethodCall = (call) => {
29925
+ const callee = call.callee;
29926
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29927
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29928
+ const methodName = callee.property.name;
29929
+ if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
29930
+ const receiverIsProvablyDate = isProvableDateExpression(callee.object);
29931
+ if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
29932
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
29933
+ return {
29934
+ node: call,
29935
+ display: `${methodName}()`
29936
+ };
29937
+ };
29938
+ const getIntlFormatterName = (expression) => {
29939
+ if (!expression) return null;
29940
+ const unwrapped = stripParenExpression(expression);
29941
+ if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
29942
+ const callee = unwrapped.callee;
29943
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29944
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
29945
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29946
+ return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
29947
+ };
29948
+ const isDeterministicIntlConstruction = (construction, formatterName) => {
29949
+ if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
29950
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
29951
+ if (formatterName !== "DateTimeFormat") return true;
29952
+ return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
29953
+ };
29954
+ const matchIntlFormatCall = (call) => {
29955
+ const callee = call.callee;
29956
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29957
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29958
+ if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
29959
+ let construction = stripParenExpression(callee.object);
29960
+ if (isNodeOfType(construction, "Identifier")) {
29961
+ const binding = findVariableInitializer(construction, construction.name);
29962
+ construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
29963
+ }
29964
+ if (!construction) return null;
29965
+ const formatterName = getIntlFormatterName(construction);
29966
+ if (!formatterName) return null;
29967
+ if (isDeterministicIntlConstruction(construction, formatterName)) return null;
29968
+ return {
29969
+ node: call,
29970
+ display: `Intl.${formatterName}().${callee.property.name}()`
29971
+ };
29972
+ };
29973
+ const isDeterministicInputDateConstruction = (expression) => {
29974
+ if (!expression) return false;
29975
+ const unwrapped = stripParenExpression(expression);
29976
+ if (!isNodeOfType(unwrapped, "NewExpression")) return false;
29977
+ if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
29978
+ return (unwrapped.arguments?.length ?? 0) > 0;
29979
+ };
29980
+ const matchDateDefaultStringification = (node) => {
29981
+ if (isNodeOfType(node, "CallExpression")) {
29982
+ const callee = node.callee;
29983
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
29984
+ node,
29985
+ display: "Date.prototype.toString()"
29986
+ };
29987
+ if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
29988
+ node,
29989
+ display: "String(new Date(…))"
29990
+ };
29991
+ return null;
29992
+ }
29993
+ if (isNodeOfType(node, "TemplateLiteral")) {
29994
+ for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
29995
+ node: expression,
29996
+ display: "`${new Date(…)}`"
29997
+ };
29998
+ }
29999
+ return null;
30000
+ };
30001
+ const findRenderPhaseComponentOrHook = (node) => {
30002
+ let functionNode = findEnclosingFunction(node);
30003
+ while (functionNode) {
30004
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
30005
+ if (!executesDuringRender(functionNode)) return null;
30006
+ functionNode = findEnclosingFunction(functionNode);
30007
+ }
30008
+ return null;
30009
+ };
30010
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30011
+ if (fileHasUseClientDirective) return true;
30012
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30013
+ if (displayName && isReactHookName(displayName)) return true;
30014
+ let callsHook = false;
30015
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30016
+ if (callsHook) return false;
30017
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30018
+ callsHook = true;
30019
+ return false;
30020
+ }
30021
+ });
30022
+ return callsHook;
30023
+ };
30024
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
30025
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30026
+ if (!isNodeOfType(body, "BlockStatement")) return false;
30027
+ const ancestors = /* @__PURE__ */ new Set();
30028
+ let cursor = node;
30029
+ while (cursor) {
30030
+ ancestors.add(cursor);
30031
+ cursor = cursor.parent ?? null;
30032
+ }
30033
+ for (const statement of body.body ?? []) {
30034
+ if (ancestors.has(statement)) return false;
30035
+ if (!isNodeOfType(statement, "IfStatement")) continue;
30036
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
30037
+ let returnsEarly = false;
30038
+ walkAst(statement.consequent, (child) => {
30039
+ if (isFunctionLike$1(child)) return false;
30040
+ if (isNodeOfType(child, "ReturnStatement")) {
30041
+ returnsEarly = true;
30042
+ return false;
30043
+ }
30044
+ });
30045
+ if (returnsEarly) return true;
30046
+ }
30047
+ return false;
30048
+ };
30049
+ const findEnclosingJsxOpeningElement = (node) => {
30050
+ let cursor = node.parent;
30051
+ while (cursor) {
30052
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30053
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
30054
+ cursor = cursor.parent ?? null;
30055
+ }
30056
+ return null;
30057
+ };
30058
+ const noLocaleFormatInRender = defineRule({
30059
+ id: "no-locale-format-in-render",
30060
+ title: "Locale/timezone formatting during render",
30061
+ severity: "warn",
30062
+ category: "Correctness",
30063
+ disabledWhen: [
30064
+ "vite",
30065
+ "cra",
30066
+ "expo",
30067
+ "react-native",
30068
+ "unknown"
30069
+ ],
30070
+ 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.",
30071
+ create: (context) => {
30072
+ if (isTestlikeFilename(context.filename)) return {};
30073
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
30074
+ let fileHasUseClientDirective = false;
30075
+ let fileIsEmailTemplate = false;
30076
+ const reportedNodes = /* @__PURE__ */ new Set();
30077
+ const reportIfRenderPhase = (match) => {
30078
+ if (reportedNodes.has(match.node)) return;
30079
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30080
+ if (!componentOrHookNode) return;
30081
+ if (fileIsEmailTemplate) return;
30082
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30083
+ if (isInsideClientOnlyGuard(match.node)) return;
30084
+ if (isGatedByFalsyInitialState(match.node)) return;
30085
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30086
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30087
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30088
+ reportedNodes.add(match.node);
30089
+ context.report({
30090
+ node: match.node,
30091
+ 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.`
30092
+ });
30093
+ };
30094
+ return {
30095
+ Program(node) {
30096
+ fileHasUseClientDirective = hasDirective(node, "use client");
30097
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30098
+ },
30099
+ CallExpression(node) {
30100
+ const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30101
+ if (match) reportIfRenderPhase(match);
30102
+ },
30103
+ TemplateLiteral(node) {
30104
+ const match = matchDateDefaultStringification(node);
30105
+ if (match) reportIfRenderPhase(match);
30106
+ },
30107
+ JSXExpressionContainer(node) {
30108
+ const expression = stripParenExpression(node.expression);
30109
+ if (!isNodeOfType(expression, "CallExpression")) return;
30110
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
30111
+ const helperName = expression.callee.name;
30112
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30113
+ if (!componentOrHookNode) return;
30114
+ const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30115
+ if (!helperNode || !isFunctionLike$1(helperNode)) return;
30116
+ if (componentOrHookDisplayNameForFunction(helperNode)) return;
30117
+ walkAst(helperNode.body ?? helperNode, (child) => {
30118
+ if (isFunctionLike$1(child)) return false;
30119
+ if (!isNodeOfType(child, "CallExpression")) return;
30120
+ const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30121
+ if (!match || reportedNodes.has(match.node)) return;
30122
+ if (fileIsEmailTemplate) return;
30123
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30124
+ if (isInsideClientOnlyGuard(node)) return;
30125
+ if (isGatedByFalsyInitialState(node)) return;
30126
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30127
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30128
+ reportedNodes.add(match.node);
30129
+ context.report({
30130
+ node: match.node,
30131
+ 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.`
30132
+ });
30133
+ });
30134
+ }
30135
+ };
30136
+ }
30137
+ });
30138
+ //#endregion
29366
30139
  //#region src/plugin/rules/design/no-long-transition-duration.ts
29367
30140
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
29368
30141
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -30176,7 +30949,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30176
30949
  "reverse",
30177
30950
  "sort"
30178
30951
  ]);
30179
- const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30180
30952
  const OBJECT_MUTATION_METHODS = new Set([
30181
30953
  "assign",
30182
30954
  "defineProperties",
@@ -30250,7 +31022,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30250
31022
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30251
31023
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30252
31024
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30253
- if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30254
31025
  }
30255
31026
  }
30256
31027
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30289,6 +31060,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30289
31060
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30290
31061
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30291
31062
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
31063
+ if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
30292
31064
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30293
31065
  });
30294
31066
  return mutations;
@@ -30521,7 +31293,7 @@ const noNestedComponentDefinition = defineRule({
30521
31293
  id: "no-nested-component-definition",
30522
31294
  title: "Component defined inside another component",
30523
31295
  tags: ["test-noise", "react-jsx-only"],
30524
- severity: "error",
31296
+ severity: "warn",
30525
31297
  category: "Correctness",
30526
31298
  recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
30527
31299
  create: (context) => {
@@ -31476,12 +32248,12 @@ const noPassDataToParent = defineRule({
31476
32248
  if (calleeNode === identifier) {
31477
32249
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
31478
32250
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
31479
- } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32251
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31480
32252
  if (!isWholePropsObjectReference(analysis, ref)) continue;
31481
32253
  if (isCustomHookParameter(ref)) continue;
31482
32254
  } else continue;
31483
32255
  const methodName = getCallMethodName(calleeNode);
31484
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32256
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31485
32257
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31486
32258
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
31487
32259
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -31672,7 +32444,7 @@ const noPassLiveStateToParent = defineRule({
31672
32444
  if (isCallResultConsumedAsArgument(callExpr)) continue;
31673
32445
  const calleeNode = callExpr.callee;
31674
32446
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
31675
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32447
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31676
32448
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31677
32449
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
31678
32450
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -32000,40 +32772,6 @@ const noPreventDefault = defineRule({
32000
32772
  }
32001
32773
  });
32002
32774
  //#endregion
32003
- //#region src/plugin/utils/is-result-discarded-call.ts
32004
- const isResultDiscardedCall = (callExpression) => {
32005
- let node = callExpression;
32006
- let parent = node.parent;
32007
- while (parent) {
32008
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
32009
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
32010
- if (isNodeOfType(parent, "ChainExpression")) {
32011
- node = parent;
32012
- parent = node.parent;
32013
- continue;
32014
- }
32015
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
32016
- node = parent;
32017
- parent = node.parent;
32018
- continue;
32019
- }
32020
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
32021
- node = parent;
32022
- parent = node.parent;
32023
- continue;
32024
- }
32025
- if (isNodeOfType(parent, "SequenceExpression")) {
32026
- const expressions = parent.expressions ?? [];
32027
- if (expressions[expressions.length - 1] !== node) return true;
32028
- node = parent;
32029
- parent = node.parent;
32030
- continue;
32031
- }
32032
- return false;
32033
- }
32034
- return false;
32035
- };
32036
- //#endregion
32037
32775
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
32038
32776
  const isRefLatchGuardedEffect = (callbackBody) => {
32039
32777
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -32240,7 +32978,7 @@ const isAlwaysFreshExpression = (expression) => {
32240
32978
  return `${callee.name}()`;
32241
32979
  }
32242
32980
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32243
- const receiver = callee.object;
32981
+ const receiver = stripParenExpression(callee.object);
32244
32982
  const property = callee.property;
32245
32983
  if (!isNodeOfType(property, "Identifier")) return null;
32246
32984
  if (isNodeOfType(receiver, "Identifier")) {
@@ -32361,8 +33099,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32361
33099
  if (typeof sourceValue !== "string") return;
32362
33100
  if (handleExtraSource?.(node, context)) return;
32363
33101
  if (sourceValue !== source) return;
33102
+ if (isTypeOnlyImport(node)) return;
32364
33103
  for (const specifier of node.specifiers ?? []) {
32365
33104
  if (isNodeOfType(specifier, "ImportSpecifier")) {
33105
+ if (specifier.importKind === "type") continue;
32366
33106
  const importedName = getImportedName$1(specifier);
32367
33107
  if (!importedName) continue;
32368
33108
  const message = messages.get(importedName);
@@ -32381,8 +33121,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32381
33121
  MemberExpression(node) {
32382
33122
  if (namespaceBindings.size === 0) return;
32383
33123
  if (node.computed) return;
32384
- if (!isNodeOfType(node.object, "Identifier")) return;
32385
- if (!namespaceBindings.has(node.object.name)) return;
33124
+ const receiver = stripParenExpression(node.object);
33125
+ if (!isNodeOfType(receiver, "Identifier")) return;
33126
+ if (!namespaceBindings.has(receiver.name)) return;
32386
33127
  if (!isNodeOfType(node.property, "Identifier")) return;
32387
33128
  const message = messages.get(node.property.name);
32388
33129
  if (message) context.report({
@@ -32447,7 +33188,7 @@ const noReactDomDeprecatedApis = defineRule({
32447
33188
  });
32448
33189
  //#endregion
32449
33190
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
32450
- const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
33191
+ const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
32451
33192
  const isVendoredShadcnUiFilename = (rawFilename) => {
32452
33193
  if (!rawFilename) return false;
32453
33194
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32485,7 +33226,7 @@ const noReact19DeprecatedApis = defineRule({
32485
33226
  requires: ["react:19"],
32486
33227
  tags: ["test-noise", "migration-hint"],
32487
33228
  severity: "warn",
32488
- recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
33229
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32489
33230
  create: (context) => {
32490
33231
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32491
33232
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -32809,34 +33550,11 @@ const noRedundantShouldComponentUpdate = defineRule({
32809
33550
  });
32810
33551
  //#endregion
32811
33552
  //#region src/plugin/rules/architecture/no-render-in-render.ts
32812
- const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32813
- if (!symbol || visitedSymbols.has(symbol)) return false;
32814
- visitedSymbols.add(symbol);
32815
- if (isComponentParameterSymbol(symbol)) return true;
32816
- if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
32817
- const source = symbol.initializer;
32818
- if (!source) return false;
32819
- return initializerRootsInProps(source, scopes, visitedSymbols);
32820
- };
32821
- const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32822
- if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
32823
- if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
32824
- return rootsInProps(node, scopes, visitedSymbols);
32825
- };
32826
- const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32827
- let current = node;
32828
- while (isNodeOfType(current, "MemberExpression")) {
32829
- if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
32830
- current = current.object;
32831
- }
32832
- if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
32833
- return false;
32834
- };
32835
33553
  const isInsideComponentContext = (node) => {
32836
33554
  let cursor = node.parent;
32837
33555
  while (cursor) {
32838
- if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
32839
33556
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33557
+ if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
32840
33558
  cursor = cursor.parent ?? null;
32841
33559
  }
32842
33560
  return false;
@@ -32846,24 +33564,28 @@ const functionBodyOf = (node) => {
32846
33564
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
32847
33565
  return null;
32848
33566
  };
33567
+ const isHookCallee = (callee) => {
33568
+ if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
33569
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
33570
+ return false;
33571
+ };
32849
33572
  const containsHookCall = (body) => {
32850
33573
  let found = false;
32851
33574
  walkAst(body, (child) => {
32852
- if (found) return;
33575
+ if (found) return false;
33576
+ if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32853
33577
  if (!isNodeOfType(child, "CallExpression")) return;
32854
- const name = getCalleeName$2(child);
32855
- if (name && isReactHookName(name)) found = true;
33578
+ if (isHookCallee(child.callee)) found = true;
32856
33579
  });
32857
33580
  return found;
32858
33581
  };
32859
- const isModuleScopeHookFreeHelper = (symbol) => {
33582
+ const isHookCallingRenderHelper = (symbol) => {
32860
33583
  if (!symbol) return false;
32861
33584
  const declaration = symbol.declarationNode;
32862
33585
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
32863
33586
  const body = functionBodyOf(declaration);
32864
33587
  if (!body) return false;
32865
- if (findEnclosingFunction(declaration) !== null) return false;
32866
- return !containsHookCall(body);
33588
+ return containsHookCall(body);
32867
33589
  };
32868
33590
  const noRenderInRender = defineRule({
32869
33591
  id: "no-render-in-render",
@@ -32872,20 +33594,13 @@ const noRenderInRender = defineRule({
32872
33594
  tags: ["test-noise"],
32873
33595
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
32874
33596
  create: (context) => ({ JSXExpressionContainer(node) {
32875
- const expression = node.expression;
33597
+ const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
32876
33598
  if (!isNodeOfType(expression, "CallExpression")) return;
32877
- let calleeName = null;
32878
- if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
32879
- else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
32880
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33599
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
33600
+ const calleeName = expression.callee.name;
33601
+ if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32881
33602
  if (!isInsideComponentContext(node)) return;
32882
- if (isNodeOfType(expression.callee, "Identifier")) {
32883
- const calleeSymbol = context.scopes.symbolFor(expression.callee);
32884
- if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
32885
- if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
32886
- } else if (isNodeOfType(expression.callee, "MemberExpression")) {
32887
- if (rootsInProps(expression.callee.object, context.scopes)) return;
32888
- }
33603
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32889
33604
  context.report({
32890
33605
  node: expression,
32891
33606
  message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
@@ -32946,13 +33661,7 @@ const noRenderPropChildren = defineRule({
32946
33661
  //#endregion
32947
33662
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
32948
33663
  const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
32949
- const isReactDomRenderCall = (node) => {
32950
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
32951
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
32952
- if (node.callee.object.name !== "ReactDOM") return false;
32953
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
32954
- return node.callee.property.name === "render";
32955
- };
33664
+ const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
32956
33665
  const isUsedAsReturnValue = (parent) => {
32957
33666
  if (!parent) return false;
32958
33667
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -33788,7 +34497,7 @@ const noSetState = defineRule({
33788
34497
  category: "Architecture",
33789
34498
  create: (context) => ({ CallExpression(node) {
33790
34499
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
33791
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
34500
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33792
34501
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
33793
34502
  if (!getParentComponent(node)) return;
33794
34503
  context.report({
@@ -36234,6 +36943,36 @@ const isTriviallyCheapExpression = (node) => {
36234
36943
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36235
36944
  return true;
36236
36945
  };
36946
+ const isTrivialContainerLiteral = (node) => {
36947
+ if (!node) return false;
36948
+ const innerExpression = stripParenExpression(node);
36949
+ if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
36950
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
36951
+ return false;
36952
+ };
36953
+ const isNonEscapingRead = (identifier) => {
36954
+ const readRoot = findTransparentExpressionRoot(identifier);
36955
+ const memberNode = readRoot.parent;
36956
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
36957
+ const memberUse = findTransparentExpressionRoot(memberNode);
36958
+ const memberUseParent = memberUse.parent;
36959
+ if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
36960
+ if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
36961
+ if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
36962
+ return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
36963
+ };
36964
+ const isMemoIdentityUnused = (memoCallNode, scopes) => {
36965
+ const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
36966
+ const parentNode = memoUsageRoot.parent;
36967
+ if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
36968
+ if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
36969
+ const bindingTarget = parentNode.id;
36970
+ if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
36971
+ if (!isNodeOfType(bindingTarget, "Identifier")) return false;
36972
+ const symbol = scopes.symbolFor(bindingTarget);
36973
+ if (!symbol) return false;
36974
+ return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
36975
+ };
36237
36976
  const noUsememoSimpleExpression = defineRule({
36238
36977
  id: "no-usememo-simple-expression",
36239
36978
  title: "useMemo on a cheap value",
@@ -36255,9 +36994,17 @@ const noUsememoSimpleExpression = defineRule({
36255
36994
  let returnExpression = null;
36256
36995
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36257
36996
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36258
- if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
36997
+ if (!returnExpression) return;
36998
+ if (isTriviallyCheapExpression(returnExpression)) {
36999
+ context.report({
37000
+ node,
37001
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37002
+ });
37003
+ return;
37004
+ }
37005
+ if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36259
37006
  node,
36260
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37007
+ message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36261
37008
  });
36262
37009
  } })
36263
37010
  });
@@ -36363,7 +37110,7 @@ const noWillUpdateSetState = defineRule({
36363
37110
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
36364
37111
  return { CallExpression(node) {
36365
37112
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
36366
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
37113
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36367
37114
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36368
37115
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36369
37116
  context.report({
@@ -36680,6 +37427,10 @@ const isRouteFactoryCall = (expression) => {
36680
37427
  }
36681
37428
  return false;
36682
37429
  };
37430
+ const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
37431
+ const expression = skipTsExpression(argument);
37432
+ return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
37433
+ });
36683
37434
  const isReactHocName = (name, state) => state.customHocs.has(name);
36684
37435
  const isHocCallee = (callee, state) => {
36685
37436
  if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
@@ -36719,7 +37470,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
36719
37470
  continue;
36720
37471
  }
36721
37472
  if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
36722
- if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
37473
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
36723
37474
  if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
36724
37475
  }
36725
37476
  return false;
@@ -36827,18 +37578,22 @@ const onlyExportComponents = defineRule({
36827
37578
  customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36828
37579
  allowExportNames: new Set(settings.allowExportNames),
36829
37580
  allowConstantExport: settings.allowConstantExport,
36830
- localComponentNames
37581
+ localComponentNames,
37582
+ scopes: context.scopes
36831
37583
  };
36832
37584
  for (const child of componentCandidates) {
36833
37585
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36834
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37586
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
36835
37587
  }
36836
37588
  if (isNodeOfType(child, "ClassDeclaration") && child.id) {
36837
37589
  if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36838
37590
  }
36839
37591
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36840
37592
  const initializer = child.init;
36841
- if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37593
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
37594
+ const expression = initializer ? skipTsExpression(initializer) : null;
37595
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
37596
+ }
36842
37597
  }
36843
37598
  }
36844
37599
  const exports = [];
@@ -36908,6 +37663,10 @@ const onlyExportComponents = defineRule({
36908
37663
  return false;
36909
37664
  })();
36910
37665
  if (isHoc && firstArgIsValid) hasReactExport = true;
37666
+ else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
37667
+ kind: "non-component",
37668
+ reportNode: stripped
37669
+ });
36911
37670
  else context.report({
36912
37671
  node: stripped,
36913
37672
  message: ANONYMOUS_MESSAGE
@@ -38503,11 +39262,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
38503
39262
  }
38504
39263
  return null;
38505
39264
  };
39265
+ const isTrivialLiteralExpression = (expression) => {
39266
+ if (isNodeOfType(expression, "Literal")) return true;
39267
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
39268
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
39269
+ if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
39270
+ return false;
39271
+ };
38506
39272
  const getSingleSetterCallFromHandler = (handler) => {
38507
39273
  const handlerStatements = getCallbackStatements(handler);
38508
39274
  if (handlerStatements.length !== 1) return null;
38509
39275
  const onlyStatement = handlerStatements[0];
38510
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
39276
+ let expression = onlyStatement;
39277
+ if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
39278
+ if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
39279
+ if (!expression) return null;
39280
+ expression = stripParenExpression(expression);
38511
39281
  if (!isNodeOfType(expression, "CallExpression")) return null;
38512
39282
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
38513
39283
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -38517,6 +39287,106 @@ const getSingleSetterCallFromHandler = (handler) => {
38517
39287
  setterArgument: expression.arguments[0]
38518
39288
  };
38519
39289
  };
39290
+ const isListenerCollectionInitializer = (init) => {
39291
+ if (!init) return false;
39292
+ if (isNodeOfType(init, "ArrayExpression")) return true;
39293
+ return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
39294
+ };
39295
+ const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
39296
+ if (!isFunctionLike$1(functionNode)) return false;
39297
+ const firstParam = functionNode.params?.[0];
39298
+ if (!isNodeOfType(firstParam, "Identifier")) return false;
39299
+ const listenerParamName = firstParam.name;
39300
+ let registersListener = false;
39301
+ walkAst(functionNode.body, (child) => {
39302
+ if (registersListener) return false;
39303
+ if (!isNodeOfType(child, "CallExpression")) return;
39304
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
39305
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
39306
+ if (!listenerCollectionNames.has(child.callee.object.name)) return;
39307
+ if (!isNodeOfType(child.callee.property, "Identifier")) return;
39308
+ if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
39309
+ const registeredArgument = child.arguments?.[0];
39310
+ if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
39311
+ });
39312
+ return registersListener;
39313
+ };
39314
+ const buildModuleScopeStoreIndex = (programRoot) => {
39315
+ const mutableBindingNames = /* @__PURE__ */ new Set();
39316
+ const listenerCollectionNames = /* @__PURE__ */ new Set();
39317
+ const moduleFunctionsByName = /* @__PURE__ */ new Map();
39318
+ if (!isNodeOfType(programRoot, "Program")) return {
39319
+ mutableBindingNames,
39320
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39321
+ };
39322
+ for (const statement of programRoot.body ?? []) {
39323
+ const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
39324
+ if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
39325
+ moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
39326
+ continue;
39327
+ }
39328
+ if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
39329
+ for (const declarator of unwrapped.declarations ?? []) {
39330
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
39331
+ const init = declarator.init ?? null;
39332
+ if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
39333
+ mutableBindingNames.add(declarator.id.name);
39334
+ continue;
39335
+ }
39336
+ if (isListenerCollectionInitializer(init)) {
39337
+ listenerCollectionNames.add(declarator.id.name);
39338
+ continue;
39339
+ }
39340
+ if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
39341
+ }
39342
+ }
39343
+ const subscribeFunctionNames = /* @__PURE__ */ new Set();
39344
+ for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
39345
+ return {
39346
+ mutableBindingNames,
39347
+ subscribeFunctionNames
39348
+ };
39349
+ };
39350
+ const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
39351
+ if (!isNodeOfType(useStateCall, "CallExpression")) return null;
39352
+ let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
39353
+ if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
39354
+ if (!isNodeOfType(initialArgument, "Identifier")) return null;
39355
+ if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
39356
+ const binding = findVariableInitializer(initialArgument, initialArgument.name);
39357
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
39358
+ return initialArgument.name;
39359
+ };
39360
+ const argumentForwardsSetter = (argument, setterName) => {
39361
+ if (!argument) return false;
39362
+ const unwrapped = stripParenExpression(argument);
39363
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
39364
+ if (!isFunctionLike$1(unwrapped)) return false;
39365
+ let callsSetter = false;
39366
+ walkAst(unwrapped.body, (child) => {
39367
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
39368
+ callsSetter = true;
39369
+ return false;
39370
+ }
39371
+ });
39372
+ return callsSetter;
39373
+ };
39374
+ const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
39375
+ let matchedCall = null;
39376
+ walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
39377
+ if (matchedCall) return false;
39378
+ if (!isNodeOfType(child, "CallExpression")) return;
39379
+ if (!isNodeOfType(child.callee, "Identifier")) return;
39380
+ if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
39381
+ const binding = findVariableInitializer(child.callee, child.callee.name);
39382
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
39383
+ for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
39384
+ matchedCall = child;
39385
+ return false;
39386
+ }
39387
+ });
39388
+ return matchedCall;
39389
+ };
38520
39390
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
38521
39391
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
38522
39392
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -38533,6 +39403,16 @@ const preferUseSyncExternalStore = defineRule({
38533
39403
  severity: "warn",
38534
39404
  recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
38535
39405
  create: (context) => {
39406
+ let cachedStoreIndex = null;
39407
+ const storeIndexFor = (node) => {
39408
+ if (cachedStoreIndex) return cachedStoreIndex;
39409
+ const programRoot = findProgramRoot(node);
39410
+ cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
39411
+ mutableBindingNames: /* @__PURE__ */ new Set(),
39412
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39413
+ };
39414
+ return cachedStoreIndex;
39415
+ };
38536
39416
  const checkComponent = (componentBody) => {
38537
39417
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38538
39418
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -38570,6 +39450,7 @@ const preferUseSyncExternalStore = defineRule({
38570
39450
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
38571
39451
  if (!useStateInitializer) continue;
38572
39452
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
39453
+ if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
38573
39454
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
38574
39455
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
38575
39456
  context.report({
@@ -38577,14 +39458,47 @@ const preferUseSyncExternalStore = defineRule({
38577
39458
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
38578
39459
  });
38579
39460
  }
39461
+ checkModuleStoreShape(componentBody, useStateBindings);
39462
+ };
39463
+ const checkModuleStoreShape = (componentBody, useStateBindings) => {
39464
+ const storeIndex = storeIndexFor(componentBody);
39465
+ if (storeIndex.mutableBindingNames.size === 0) return;
39466
+ if (storeIndex.subscribeFunctionNames.size === 0) return;
39467
+ const snapshotBindings = useStateBindings.map((binding) => ({
39468
+ binding,
39469
+ storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
39470
+ })).filter((candidate) => candidate.storeName !== null);
39471
+ if (snapshotBindings.length === 0) return;
39472
+ const reportedDeclarators = /* @__PURE__ */ new Set();
39473
+ for (const effectCall of findUseEffectsInComponent(componentBody)) {
39474
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
39475
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
39476
+ const depsNode = effectCall.arguments[1];
39477
+ if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
39478
+ if ((depsNode.elements?.length ?? 0) !== 0) continue;
39479
+ const callback = getEffectCallback(effectCall);
39480
+ if (!callback || !isFunctionLike$1(callback)) continue;
39481
+ for (const { binding, storeName } of snapshotBindings) {
39482
+ if (reportedDeclarators.has(binding.declarator)) continue;
39483
+ if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
39484
+ reportedDeclarators.add(binding.declarator);
39485
+ context.report({
39486
+ node: binding.declarator,
39487
+ 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.`
39488
+ });
39489
+ }
39490
+ }
38580
39491
  };
38581
39492
  return {
38582
39493
  FunctionDeclaration(node) {
38583
- if (!node.id?.name || !isUppercaseName(node.id.name)) return;
39494
+ const functionName = node.id?.name;
39495
+ if (!functionName) return;
39496
+ if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38584
39497
  checkComponent(node.body);
38585
39498
  },
38586
39499
  VariableDeclarator(node) {
38587
- if (!isComponentAssignment(node)) return;
39500
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
39501
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
38588
39502
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
38589
39503
  checkComponent(node.init.body);
38590
39504
  }
@@ -39194,7 +40108,7 @@ const queryNoVoidQueryFn = defineRule({
39194
40108
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
39195
40109
  const body = queryFnValue.body;
39196
40110
  if (!isNodeOfType(body, "BlockStatement")) return;
39197
- if ((body.body ?? []).length === 0) context.report({
40111
+ if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39198
40112
  node: queryFnProperty,
39199
40113
  message: "This empty queryFn caches undefined, so the component never gets data."
39200
40114
  });
@@ -39701,17 +40615,6 @@ const renderingHoistJsx = defineRule({
39701
40615
  }
39702
40616
  });
39703
40617
  //#endregion
39704
- //#region src/plugin/utils/find-declarator-for-binding.ts
39705
- const findDeclaratorForBinding = (bindingIdentifier) => {
39706
- let ancestor = bindingIdentifier.parent;
39707
- while (ancestor) {
39708
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
39709
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
39710
- ancestor = ancestor.parent ?? null;
39711
- }
39712
- return null;
39713
- };
39714
- //#endregion
39715
40618
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
39716
40619
  const NONDETERMINISTIC_RENDER_PATTERNS = [
39717
40620
  {
@@ -39720,19 +40623,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
39720
40623
  },
39721
40624
  {
39722
40625
  display: "Date.now()",
39723
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Date" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40626
+ matches: (node) => isGlobalMethodCall(node, "Date", "now")
39724
40627
  },
39725
40628
  {
39726
40629
  display: "Math.random()",
39727
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Math" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "random"
40630
+ matches: (node) => isGlobalMethodCall(node, "Math", "random")
39728
40631
  },
39729
40632
  {
39730
40633
  display: "performance.now()",
39731
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "performance" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40634
+ matches: (node) => isGlobalMethodCall(node, "performance", "now")
39732
40635
  },
39733
40636
  {
39734
40637
  display: "crypto.randomUUID()",
39735
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "crypto" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "randomUUID"
40638
+ matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39736
40639
  }
39737
40640
  ];
39738
40641
  const findOpeningElementOfChild = (jsxNode) => {
@@ -39744,54 +40647,6 @@ const findOpeningElementOfChild = (jsxNode) => {
39744
40647
  }
39745
40648
  return null;
39746
40649
  };
39747
- const executesDuringRender = (functionNode) => {
39748
- const parent = functionNode.parent;
39749
- if (!isNodeOfType(parent, "CallExpression")) return false;
39750
- if (parent.callee === functionNode) return true;
39751
- return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
39752
- };
39753
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
39754
- const referencesClientOnlyFlag = (expression) => {
39755
- const unwrapped = stripParenExpression(expression);
39756
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
39757
- if (isNodeOfType(unwrapped, "MemberExpression")) {
39758
- const property = unwrapped.property;
39759
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
39760
- }
39761
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
39762
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
39763
- return false;
39764
- };
39765
- const isFalsyLiteral = (node) => {
39766
- if (!node) return true;
39767
- if (isNodeOfType(node, "Literal")) return !node.value;
39768
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
39769
- };
39770
- const isFalsyInitialStateBinding = (identifier) => {
39771
- if (!isNodeOfType(identifier, "Identifier")) return false;
39772
- const binding = findVariableInitializer(identifier, identifier.name);
39773
- if (!binding) return false;
39774
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
39775
- if (!declarator?.init) return false;
39776
- const init = stripParenExpression(declarator.init);
39777
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
39778
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
39779
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
39780
- return isFalsyLiteral(init.arguments?.[0]);
39781
- };
39782
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
39783
- const isGatedByFalsyInitialState = (node) => {
39784
- let cursor = node;
39785
- let parent = node.parent;
39786
- while (parent) {
39787
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
39788
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39789
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39790
- cursor = parent;
39791
- parent = parent.parent ?? null;
39792
- }
39793
- return false;
39794
- };
39795
40650
  const isYearOnlyDateRead = (dateNode) => {
39796
40651
  const member = dateNode.parent;
39797
40652
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -39815,23 +40670,6 @@ const isInsideMotionTransitionAttribute = (node) => {
39815
40670
  }
39816
40671
  return false;
39817
40672
  };
39818
- const isInsideClientOnlyGuard = (node) => {
39819
- let cursor = node;
39820
- let parent = node.parent;
39821
- while (parent) {
39822
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
39823
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
39824
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
39825
- cursor = parent;
39826
- parent = parent.parent ?? null;
39827
- }
39828
- return false;
39829
- };
39830
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
39831
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
39832
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
39833
- return false;
39834
- };
39835
40673
  const renderingHydrationMismatchTime = defineRule({
39836
40674
  id: "rendering-hydration-mismatch-time",
39837
40675
  title: "Time or random value in JSX",
@@ -39879,6 +40717,35 @@ const renderingHydrationMismatchTime = defineRule({
39879
40717
  }
39880
40718
  });
39881
40719
  //#endregion
40720
+ //#region src/plugin/utils/contains-locale-environment-read.ts
40721
+ const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
40722
+ "toLocaleString",
40723
+ "toLocaleDateString",
40724
+ "toLocaleTimeString",
40725
+ "getTimezoneOffset"
40726
+ ]);
40727
+ const containsLocaleEnvironmentRead = (expression) => {
40728
+ let readsLocaleEnvironment = false;
40729
+ walkAst(expression, (child) => {
40730
+ if (readsLocaleEnvironment) return false;
40731
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
40732
+ if (child.object.name === "Intl") {
40733
+ readsLocaleEnvironment = true;
40734
+ return false;
40735
+ }
40736
+ if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
40737
+ readsLocaleEnvironment = true;
40738
+ return false;
40739
+ }
40740
+ }
40741
+ 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)) {
40742
+ readsLocaleEnvironment = true;
40743
+ return false;
40744
+ }
40745
+ });
40746
+ return readsLocaleEnvironment;
40747
+ };
40748
+ //#endregion
39882
40749
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
39883
40750
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
39884
40751
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -39944,14 +40811,15 @@ const renderingHydrationNoFlicker = defineRule({
39944
40811
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
39945
40812
  const callback = getEffectCallback(node);
39946
40813
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
39947
- const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
39948
- if (!bodyStatements || bodyStatements.length !== 1) return;
40814
+ const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
40815
+ if (bodyStatements.length !== 1) return;
39949
40816
  const soleStatement = bodyStatements[0];
39950
40817
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
39951
40818
  const expression = soleStatement.expression;
39952
40819
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
39953
40820
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
39954
40821
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
40822
+ if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
39955
40823
  context.report({
39956
40824
  node,
39957
40825
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -40770,6 +41638,9 @@ const rerenderFunctionalSetstate = defineRule({
40770
41638
  } })
40771
41639
  });
40772
41640
  //#endregion
41641
+ //#region src/plugin/utils/is-trivial-built-in-construction.ts
41642
+ const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
41643
+ //#endregion
40773
41644
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
40774
41645
  const rerenderLazyRefInit = defineRule({
40775
41646
  id: "rerender-lazy-ref-init",
@@ -40780,7 +41651,7 @@ const rerenderLazyRefInit = defineRule({
40780
41651
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
40781
41652
  create: (context) => ({ CallExpression(node) {
40782
41653
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
40783
- const initializer = node.arguments[0];
41654
+ const initializer = stripParenExpression(node.arguments[0]);
40784
41655
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
40785
41656
  const isNewCall = isNodeOfType(initializer, "NewExpression");
40786
41657
  if (!isPlainCall && !isNewCall) return;
@@ -40788,7 +41659,7 @@ const rerenderLazyRefInit = defineRule({
40788
41659
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40789
41660
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40790
41661
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40791
- if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41662
+ if (isTrivialBuiltInConstruction(initializer)) return;
40792
41663
  if (isPlainCall && isReactHookName(calleeName)) return;
40793
41664
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40794
41665
  context.report({
@@ -40824,11 +41695,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40824
41695
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40825
41696
  const findEagerInitializerCall = (expression, depth = 0) => {
40826
41697
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
40827
- if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
40828
- if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
40829
- if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
40830
- if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
40831
- if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
41698
+ const innerExpression = stripParenExpression(expression);
41699
+ if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
41700
+ if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
41701
+ if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
41702
+ if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
40832
41703
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
40833
41704
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
40834
41705
  if (spreadCall) return spreadCall;
@@ -40851,7 +41722,7 @@ const rerenderLazyStateInit = defineRule({
40851
41722
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40852
41723
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40853
41724
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40854
- if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41725
+ if (isTrivialBuiltInConstruction(initializer)) return;
40855
41726
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
40856
41727
  if (isReactHookName(calleeName)) return;
40857
41728
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -41518,7 +42389,7 @@ const rnAnimationReactionAsDerived = defineRule({
41518
42389
  const body = reactionFn.body;
41519
42390
  let singleAssignment = null;
41520
42391
  if (isNodeOfType(body, "BlockStatement")) {
41521
- const statements = body.body ?? [];
42392
+ const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41522
42393
  if (statements.length !== 1) return;
41523
42394
  const onlyStatement = statements[0];
41524
42395
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -41592,7 +42463,8 @@ const PROMISE_SETTLE_METHODS = new Set([
41592
42463
  "catch",
41593
42464
  "finally"
41594
42465
  ]);
41595
- const findChainRoot = (node) => {
42466
+ const findChainRoot = (wrappedNode) => {
42467
+ const node = stripParenExpression(wrappedNode);
41596
42468
  if (isNodeOfType(node, "CallExpression")) {
41597
42469
  if (isNodeOfType(node.callee, "Identifier")) return {
41598
42470
  calleeName: node.callee.name,
@@ -42180,20 +43052,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
42180
43052
  };
42181
43053
  const isReactNativeDimensionsCallee = (node, callee) => {
42182
43054
  if (!isNodeOfType(callee, "MemberExpression")) return false;
42183
- if (isNodeOfType(callee.object, "Identifier")) {
42184
- const localName = callee.object.name;
43055
+ const receiver = stripParenExpression(callee.object);
43056
+ if (isNodeOfType(receiver, "Identifier")) {
43057
+ const localName = receiver.name;
42185
43058
  const binding = findVariableInitializer(node, localName);
42186
43059
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
42187
43060
  return localName === "Dimensions";
42188
43061
  }
42189
- if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
42190
- if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
42191
- const rootName = getRootIdentifierName(callee.object.object);
43062
+ if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
43063
+ if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
43064
+ const rootName = getRootIdentifierName(receiver.object);
42192
43065
  if (rootName === null) return false;
42193
43066
  const importBinding = getImportBindingForName(node, rootName);
42194
43067
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
42195
43068
  }
42196
- if (getRequireCallSource(callee.object) === REACT_NATIVE_MODULE) return isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions";
43069
+ if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42197
43070
  return false;
42198
43071
  };
42199
43072
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -42637,7 +43510,8 @@ const rnNoLegacyShadowStyles = defineRule({
42637
43510
  },
42638
43511
  CallExpression(node) {
42639
43512
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
42640
- if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
43513
+ const receiver = stripParenExpression(node.callee.object);
43514
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42641
43515
  if (!isMemberProperty(node.callee, "create")) return;
42642
43516
  const stylesArgument = node.arguments?.[0];
42643
43517
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -43487,7 +44361,7 @@ const rnNoSetNativeProps = defineRule({
43487
44361
  const callee = node.callee;
43488
44362
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
43489
44363
  if (!isNodeOfType(callee, "MemberExpression")) return;
43490
- if (!isStaticMemberNamed(callee.object, "current")) return;
44364
+ if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43491
44365
  context.report({
43492
44366
  node,
43493
44367
  message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
@@ -43715,14 +44589,15 @@ const analyzeGestureChain = (expression) => {
43715
44589
  if (!isNodeOfType(callee, "MemberExpression")) return null;
43716
44590
  if (!isNodeOfType(callee.property, "Identifier")) return null;
43717
44591
  const methodName = callee.property.name;
43718
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
44592
+ const receiver = stripParenExpression(callee.object);
44593
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43719
44594
  factoryName: methodName,
43720
44595
  chainMethodNames,
43721
44596
  numberOfTapsArgument
43722
44597
  };
43723
44598
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
43724
44599
  chainMethodNames.push(methodName);
43725
- cursor = callee.object;
44600
+ cursor = receiver;
43726
44601
  }
43727
44602
  return null;
43728
44603
  };
@@ -47388,6 +48263,8 @@ const roleSupportsAriaProps = defineRule({
47388
48263
  const propName = propRawName.toLowerCase();
47389
48264
  if (!propName.startsWith("aria-")) continue;
47390
48265
  if (!ARIA_PROPERTIES.has(propName)) continue;
48266
+ const attributeValue = attributeNode.value;
48267
+ if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
47391
48268
  (ariaAttributes ??= []).push({
47392
48269
  attribute,
47393
48270
  propName
@@ -47757,21 +48634,6 @@ const isInsideLoop = (descendant, ancestor) => {
47757
48634
  }
47758
48635
  return false;
47759
48636
  };
47760
- const isUseEffectEventSymbol = (symbol) => {
47761
- const initializer = symbol.initializer;
47762
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47763
- return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47764
- };
47765
- const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47766
- const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47767
- return Boolean(symbol && symbol.kind !== "import");
47768
- };
47769
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
47770
- const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47771
- const initializer = symbol.initializer;
47772
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47773
- return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47774
- };
47775
48637
  const findEnclosingComponentOrHookFunction = (node) => {
47776
48638
  let current = node.parent;
47777
48639
  while (current) {
@@ -47961,8 +48823,7 @@ const rulesOfHooks = defineRule({
47961
48823
  },
47962
48824
  Identifier(node) {
47963
48825
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47964
- if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47965
- if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
48826
+ if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
47966
48827
  if (!isSameComponentOrHookScope(symbol, node)) return;
47967
48828
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47968
48829
  context.report({
@@ -48110,7 +48971,8 @@ const serverAfterNonblocking = defineRule({
48110
48971
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48111
48972
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48112
48973
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48113
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
48974
+ const receiver = stripParenExpression(node.callee.object);
48975
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48114
48976
  if (!objectName) return;
48115
48977
  const methodName = node.callee.property.name;
48116
48978
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -48786,7 +49648,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
48786
49648
  };
48787
49649
  const ascendMemberChain = (referenceIdentifier) => {
48788
49650
  let chainTip = referenceIdentifier;
48789
- while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
49651
+ while (chainTip.parent) {
49652
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
49653
+ chainTip = chainTip.parent;
49654
+ continue;
49655
+ }
49656
+ if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
49657
+ chainTip = chainTip.parent;
49658
+ continue;
49659
+ }
49660
+ break;
49661
+ }
48790
49662
  return chainTip;
48791
49663
  };
48792
49664
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -48811,7 +49683,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
48811
49683
  const callee = callExpression.callee;
48812
49684
  if (isNodeOfType(callee, "MemberExpression")) {
48813
49685
  const methodName = getMemberPropertyName$1(callee);
48814
- return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
49686
+ const calleeReceiver = stripParenExpression(callee.object);
49687
+ return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
48815
49688
  }
48816
49689
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
48817
49690
  const calleeSymbol = scopes.symbolFor(callee);
@@ -49541,7 +50414,7 @@ const walkServerFnChain = (outerNode) => {
49541
50414
  };
49542
50415
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
49543
50416
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
49544
- let currentNode = outerNode.callee.object;
50417
+ let currentNode = stripParenExpression(outerNode.callee.object);
49545
50418
  while (isNodeOfType(currentNode, "CallExpression")) {
49546
50419
  const calleeName = getCalleeName$2(currentNode);
49547
50420
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -49552,7 +50425,7 @@ const walkServerFnChain = (outerNode) => {
49552
50425
  }
49553
50426
  }
49554
50427
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
49555
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
50428
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49556
50429
  else break;
49557
50430
  }
49558
50431
  return result;
@@ -50205,7 +51078,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
50205
51078
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
50206
51079
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
50207
51080
  if (methodName) methodNames.unshift(methodName);
50208
- currentNode = currentNode.callee.object;
51081
+ currentNode = stripParenExpression(currentNode.callee.object);
50209
51082
  }
50210
51083
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
50211
51084
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -53287,6 +54160,18 @@ const reactDoctorRules = [
53287
54160
  category: "Bugs"
53288
54161
  }
53289
54162
  },
54163
+ {
54164
+ key: "react-doctor/no-locale-format-in-render",
54165
+ id: "no-locale-format-in-render",
54166
+ source: "react-doctor",
54167
+ originallyExternal: false,
54168
+ rule: {
54169
+ ...noLocaleFormatInRender,
54170
+ framework: "global",
54171
+ category: "Bugs",
54172
+ requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
54173
+ }
54174
+ },
53290
54175
  {
53291
54176
  key: "react-doctor/no-long-transition-duration",
53292
54177
  id: "no-long-transition-duration",
@@ -56163,6 +57048,7 @@ const CROSS_FILE_RULE_IDS = new Set([
56163
57048
  "nextjs-no-use-search-params-without-suspense",
56164
57049
  "no-dynamic-import-path",
56165
57050
  "no-full-lodash-import",
57051
+ "no-locale-format-in-render",
56166
57052
  "no-mutating-reducer-state",
56167
57053
  "prefer-dynamic-import",
56168
57054
  "rendering-hydration-mismatch-time",
@@ -56297,6 +57183,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
56297
57183
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
56298
57184
  ["no-dynamic-import-path", collectNearestManifestDependencies],
56299
57185
  ["no-full-lodash-import", collectNearestManifestDependencies],
57186
+ ["no-locale-format-in-render", collectNearestManifestDependencies],
56300
57187
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
56301
57188
  ["prefer-dynamic-import", collectNearestManifestDependencies],
56302
57189
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],