oxlint-plugin-react-doctor 0.7.4-dev.118f806 → 0.7.4-dev.2b5a18e

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 +138 -0
  2. package/dist/index.js +1470 -361
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9191,6 +9191,24 @@ const fileContainsReleaseForUsage = (usage, context) => {
9191
9191
  });
9192
9192
  return didFindRelease;
9193
9193
  };
9194
+ const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
9195
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
9196
+ if (!bindingIdentifier) return false;
9197
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
9198
+ const isSubscribeBinding = (candidateBinding) => {
9199
+ const symbol = context.scopes.symbolFor(candidateBinding);
9200
+ if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.length === 0) return false;
9201
+ visitedSymbolIds.add(symbol.id);
9202
+ return symbol.references.every((reference) => {
9203
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
9204
+ const referenceParent = referenceRoot.parent;
9205
+ if (isNodeOfType(referenceParent, "CallExpression") && referenceParent.arguments?.[0] === referenceRoot) return isReactApiCall(referenceParent, "useSyncExternalStore", context.scopes);
9206
+ const aliasDeclaration = referenceParent?.parent;
9207
+ return Boolean(isNodeOfType(referenceParent, "VariableDeclarator") && referenceParent.init === referenceRoot && isNodeOfType(referenceParent.id, "Identifier") && isNodeOfType(aliasDeclaration, "VariableDeclaration") && aliasDeclaration.kind === "const" && isSubscribeBinding(referenceParent.id));
9208
+ });
9209
+ };
9210
+ return isSubscribeBinding(bindingIdentifier);
9211
+ };
9194
9212
  const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
9195
9213
  let currentNode = resourceNode;
9196
9214
  let parentNode = currentNode.parent;
@@ -9212,6 +9230,8 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
9212
9230
  if (!body) return null;
9213
9231
  let leak = null;
9214
9232
  const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
9233
+ const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
9234
+ const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
9215
9235
  walkAst(body, (child) => {
9216
9236
  if (leak !== null) return false;
9217
9237
  if (isFunctionLike$1(child)) return false;
@@ -9226,7 +9246,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
9226
9246
  eventKey: null,
9227
9247
  handlerKey: null
9228
9248
  };
9229
- if (!fileContainsReleaseForUsage(socketUsage, context)) {
9249
+ if (!hasReleaseForUsage(socketUsage)) {
9230
9250
  leak = socketUsage;
9231
9251
  return false;
9232
9252
  }
@@ -9243,7 +9263,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
9243
9263
  eventKey: null,
9244
9264
  handlerKey: null
9245
9265
  };
9246
- if (!fileContainsReleaseForUsage(timerUsage, context)) {
9266
+ if (!hasReleaseForUsage(timerUsage)) {
9247
9267
  leak = timerUsage;
9248
9268
  return false;
9249
9269
  }
@@ -9257,7 +9277,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
9257
9277
  handleKey: findAssignedResourceKey(child, context),
9258
9278
  ...registrationDetails
9259
9279
  };
9260
- if (!hasSelfReleasingListenerOptions(child, context) && !fileContainsReleaseForUsage(subscriptionUsage, context)) leak = subscriptionUsage;
9280
+ if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
9261
9281
  return false;
9262
9282
  }
9263
9283
  });
@@ -9948,13 +9968,18 @@ const unwrapExpression$3 = (node) => {
9948
9968
  return current;
9949
9969
  };
9950
9970
  /**
9951
- * Get the hook name from a call expression's callee, regardless of
9952
- * whether the hook is called as `useFoo()` (Identifier) or
9953
- * `React.useFoo()` (MemberExpression).
9971
+ * Get the hook name from a direct, wrapped, namespaced, or immutable
9972
+ * React import alias call.
9954
9973
  */
9955
- const getHookName = (callee) => {
9956
- if (isNodeOfType(callee, "Identifier")) return callee.name;
9957
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9974
+ const getHookName = (callee, scopes) => {
9975
+ const strippedCallee = unwrapExpression$3(callee);
9976
+ if (isNodeOfType(strippedCallee, "Identifier")) {
9977
+ const resolvedSymbol = scopes ? resolveConstIdentifierAlias(strippedCallee, scopes) : null;
9978
+ const importDeclaration = resolvedSymbol?.declarationNode.parent;
9979
+ if (resolvedSymbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react") return getImportedName(resolvedSymbol.declarationNode) ?? strippedCallee.name;
9980
+ return strippedCallee.name;
9981
+ }
9982
+ if (isNodeOfType(strippedCallee, "MemberExpression") && !strippedCallee.computed && isNodeOfType(strippedCallee.property, "Identifier")) return strippedCallee.property.name;
9958
9983
  return null;
9959
9984
  };
9960
9985
  const FUNCTION_SCOPE_KINDS = new Set([
@@ -10139,7 +10164,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
10139
10164
  * - primitive-literal local consts (the value never changes
10140
10165
  * between renders unless the literal does)
10141
10166
  */
10142
- const symbolHasStableHookOrigin = (symbol) => {
10167
+ const symbolHasStableHookOrigin = (symbol, scopes) => {
10143
10168
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
10144
10169
  let declarator = symbol.declarationNode;
10145
10170
  while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
@@ -10152,7 +10177,7 @@ const symbolHasStableHookOrigin = (symbol) => {
10152
10177
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
10153
10178
  }
10154
10179
  if (!isNodeOfType(initializer, "CallExpression")) return false;
10155
- const initializerHookName = getHookName(initializer.callee);
10180
+ const initializerHookName = getHookName(initializer.callee, scopes);
10156
10181
  if (!initializerHookName) return false;
10157
10182
  if (initializerHookName === "useRef") return true;
10158
10183
  if (initializerHookName === "useEffectEvent") return true;
@@ -10187,7 +10212,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
10187
10212
  if (!declarator.init) return false;
10188
10213
  const initializer = unwrapExpression$3(declarator.init);
10189
10214
  if (!isNodeOfType(initializer, "CallExpression")) return false;
10190
- const initializerHookName = getHookName(initializer.callee);
10215
+ const initializerHookName = getHookName(initializer.callee, scopes);
10191
10216
  if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
10192
10217
  const depsArgument = initializer.arguments[1];
10193
10218
  if (!depsArgument || !isAstNode(depsArgument)) return false;
@@ -10217,7 +10242,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
10217
10242
  }
10218
10243
  return null;
10219
10244
  };
10220
- const isStableRefContainerCapture = (symbol, depKey) => {
10245
+ const isStableRefContainerCapture = (symbol, depKey, scopes) => {
10221
10246
  if (symbol.kind !== "const") return false;
10222
10247
  if (!depKey.startsWith(`${symbol.name}.`)) return false;
10223
10248
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
@@ -10231,7 +10256,7 @@ const isStableRefContainerCapture = (symbol, depKey) => {
10231
10256
  if (!propertyValue) return false;
10232
10257
  currentValue = propertyValue;
10233
10258
  }
10234
- return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
10259
+ return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
10235
10260
  };
10236
10261
  const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
10237
10262
  if (visitedSymbolIds.has(symbol.id)) return true;
@@ -10249,7 +10274,13 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
10249
10274
  }
10250
10275
  return true;
10251
10276
  };
10252
- const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
10277
+ const symbolHasStableImportedAlias = (symbol, scopes) => {
10278
+ if (symbol.kind !== "const") return false;
10279
+ if (symbol.references.some((reference) => reference.flag !== "read")) return false;
10280
+ const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
10281
+ return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
10282
+ };
10283
+ const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
10253
10284
  //#endregion
10254
10285
  //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
10255
10286
  const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
@@ -10437,10 +10468,18 @@ const collectCaptureDepKeys = (callback, scopes) => {
10437
10468
  }
10438
10469
  const depKey = computeDepKey(reference);
10439
10470
  if (!depKey) continue;
10440
- if (isStableRefContainerCapture(symbol, depKey)) {
10471
+ if (isStableRefContainerCapture(symbol, depKey, scopes)) {
10441
10472
  stableCapturedNames.add(depKey);
10442
10473
  continue;
10443
10474
  }
10475
+ if (depKey === symbol.name) {
10476
+ const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
10477
+ if (identitySourceKeys) {
10478
+ if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
10479
+ for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
10480
+ continue;
10481
+ }
10482
+ }
10444
10483
  keys.add(depKey);
10445
10484
  }
10446
10485
  return {
@@ -10469,12 +10508,60 @@ const hasComputedMemberExpression = (node) => {
10469
10508
  if (stripped.computed) return true;
10470
10509
  return hasComputedMemberExpression(stripped.object);
10471
10510
  };
10511
+ const mergeIdentitySourceKeys = (expressions, scopes, visitedSymbolIds) => {
10512
+ const identitySourceKeys = /* @__PURE__ */ new Set();
10513
+ for (const expression of expressions) {
10514
+ const expressionSourceKeys = resolveIdentitySourceKeysFromExpression(expression, scopes, visitedSymbolIds);
10515
+ if (!expressionSourceKeys) return null;
10516
+ for (const expressionSourceKey of expressionSourceKeys) identitySourceKeys.add(expressionSourceKey);
10517
+ }
10518
+ return identitySourceKeys;
10519
+ };
10520
+ const resolveIdentitySourceKeysFromExpression = (expression, scopes, visitedSymbolIds) => {
10521
+ const stripped = unwrapExpression$3(expression);
10522
+ if (isNodeOfType(stripped, "Literal") && (stripped.value === null || typeof stripped.value === "string" || typeof stripped.value === "number" || typeof stripped.value === "boolean") || isNodeOfType(stripped, "TemplateLiteral") && getStaticTemplateLiteralValue(stripped) !== null) return /* @__PURE__ */ new Set();
10523
+ if (isNodeOfType(stripped, "Identifier")) {
10524
+ const sourceSymbol = scopes.symbolFor(stripped);
10525
+ if (!sourceSymbol) return null;
10526
+ if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
10527
+ if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((reference) => reference.flag === "read")) {
10528
+ if (visitedSymbolIds.has(sourceSymbol.id)) return null;
10529
+ visitedSymbolIds.add(sourceSymbol.id);
10530
+ const sourceKeys = resolveIdentitySourceKeysFromExpression(sourceSymbol.initializer, scopes, visitedSymbolIds);
10531
+ visitedSymbolIds.delete(sourceSymbol.id);
10532
+ if (sourceKeys) return sourceKeys;
10533
+ }
10534
+ return new Set([sourceSymbol.name]);
10535
+ }
10536
+ if (isNodeOfType(stripped, "MemberExpression")) {
10537
+ if (hasComputedMemberExpression(stripped)) return null;
10538
+ const sourceKey = stringifyMemberChain(stripped);
10539
+ const rootIdentifier = getMemberRootIdentifier(stripped);
10540
+ const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
10541
+ if (!sourceKey || !rootSymbol) return null;
10542
+ if (isOutsideAllFunctions(rootSymbol)) return /* @__PURE__ */ new Set();
10543
+ if (isStableRefContainerCapture(rootSymbol, sourceKey, scopes)) return /* @__PURE__ */ new Set();
10544
+ if (symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
10545
+ return new Set([sourceKey]);
10546
+ }
10547
+ if (isNodeOfType(stripped, "LogicalExpression")) return mergeIdentitySourceKeys([stripped.left, stripped.right], scopes, visitedSymbolIds);
10548
+ if (isNodeOfType(stripped, "ConditionalExpression")) return mergeIdentitySourceKeys([
10549
+ stripped.test,
10550
+ stripped.consequent,
10551
+ stripped.alternate
10552
+ ], scopes, visitedSymbolIds);
10553
+ return null;
10554
+ };
10555
+ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
10556
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
10557
+ return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
10558
+ };
10472
10559
  const isUseCallbackResultDep = (node, scopes) => {
10473
10560
  const rootSymbol = getRootSymbol(node, scopes);
10474
10561
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
10475
- return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
10562
+ return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
10476
10563
  };
10477
- const isExtraEffectDepAllowed = (node, scopes) => {
10564
+ const isExtraReactiveDepAllowed = (node, scopes) => {
10478
10565
  const rootIdentifier = getMemberRootIdentifier(node);
10479
10566
  if (!rootIdentifier) return false;
10480
10567
  const symbol = scopes.symbolFor(rootIdentifier);
@@ -10502,12 +10589,78 @@ const isUnstableInitializer = (node) => {
10502
10589
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
10503
10590
  return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
10504
10591
  };
10592
+ const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
10593
+ const candidate = unwrapExpression$3(node);
10594
+ if (isUnstableInitializer(candidate)) return true;
10595
+ if (isNodeOfType(candidate, "ConditionalExpression")) return isPotentiallyFreshComparedValue(candidate.consequent, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.alternate, scopes, visitedSymbolIds);
10596
+ if (isNodeOfType(candidate, "LogicalExpression")) return isPotentiallyFreshComparedValue(candidate.left, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.right, scopes, visitedSymbolIds);
10597
+ if (!isNodeOfType(candidate, "Identifier")) return false;
10598
+ const symbol = scopes.symbolFor(candidate);
10599
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
10600
+ if (symbol.kind === "let" || symbol.kind === "var") return true;
10601
+ if (symbol.kind !== "const" || !symbol.initializer) return false;
10602
+ visitedSymbolIds.add(symbol.id);
10603
+ return isPotentiallyFreshComparedValue(symbol.initializer, scopes, visitedSymbolIds);
10604
+ };
10605
+ const isExtraDepAllowedForHook = (hookName, node, scopes) => {
10606
+ if (!isExtraReactiveDepAllowed(node, scopes)) return false;
10607
+ if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
10608
+ if (hookName !== "useMemo") return false;
10609
+ const rootSymbol = getRootSymbol(node, scopes);
10610
+ return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
10611
+ };
10505
10612
  const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
10506
10613
  const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
10507
- const isStableSetterLikeSymbol = (symbol) => {
10508
- if (!symbolHasStableHookOrigin(symbol)) return false;
10614
+ const isStableSetterLikeSymbol = (symbol, scopes) => {
10615
+ if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
10509
10616
  return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
10510
10617
  };
10618
+ const isConvergingFunctionalUpdater = (node, scopes) => {
10619
+ const updater = unwrapExpression$3(node);
10620
+ if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
10621
+ const previousValueParameter = updater.params?.[0];
10622
+ if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
10623
+ let returnedExpression = updater.body;
10624
+ if (isNodeOfType(updater.body, "BlockStatement")) {
10625
+ returnedExpression = null;
10626
+ for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
10627
+ returnedExpression = statement.argument;
10628
+ break;
10629
+ }
10630
+ }
10631
+ const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
10632
+ if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
10633
+ const test = unwrapExpression$3(conditional.test);
10634
+ if (!isNodeOfType(test, "BinaryExpression") || ![
10635
+ "===",
10636
+ "!==",
10637
+ "==",
10638
+ "!="
10639
+ ].includes(test.operator)) return false;
10640
+ const isPreviousValue = (expression) => {
10641
+ const candidate = unwrapExpression$3(expression);
10642
+ return isNodeOfType(candidate, "Identifier") && candidate.name === previousValueParameter.name;
10643
+ };
10644
+ let comparedValue = null;
10645
+ if (isPreviousValue(test.left)) comparedValue = test.right;
10646
+ else if (isPreviousValue(test.right)) comparedValue = test.left;
10647
+ if (!comparedValue) return false;
10648
+ if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
10649
+ const isSameComparedValue = (expression) => {
10650
+ const candidate = unwrapExpression$3(expression);
10651
+ const compared = unwrapExpression$3(comparedValue);
10652
+ if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
10653
+ if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
10654
+ return false;
10655
+ };
10656
+ return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
10657
+ };
10658
+ const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
10659
+ const parent = identifier.parent;
10660
+ if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
10661
+ const writtenValue = parent.arguments?.[0];
10662
+ return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
10663
+ };
10511
10664
  const findStableSetterReference = (node, scopes) => {
10512
10665
  let setterName = null;
10513
10666
  const visit = (current) => {
@@ -10515,7 +10668,7 @@ const findStableSetterReference = (node, scopes) => {
10515
10668
  if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
10516
10669
  if (isNodeOfType(current, "Identifier")) {
10517
10670
  const symbol = scopes.referenceFor(current)?.resolvedSymbol;
10518
- if (symbol && isStableSetterLikeSymbol(symbol)) {
10671
+ if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
10519
10672
  setterName = symbol.name;
10520
10673
  return;
10521
10674
  }
@@ -10631,11 +10784,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
10631
10784
  }
10632
10785
  return false;
10633
10786
  };
10634
- const isSeededDataRefSymbol = (refSymbol) => {
10787
+ const isSeededDataRefSymbol = (refSymbol, scopes) => {
10635
10788
  if (!refSymbol) return false;
10636
10789
  const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
10637
10790
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
10638
- if (getHookName(initializer.callee) !== "useRef") return false;
10791
+ if (getHookName(initializer.callee, scopes) !== "useRef") return false;
10639
10792
  const firstArgument = initializer.arguments[0];
10640
10793
  if (!firstArgument || !isAstNode(firstArgument)) return false;
10641
10794
  const strippedArgument = unwrapExpression$3(firstArgument);
@@ -10675,7 +10828,9 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
10675
10828
  const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
10676
10829
  return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
10677
10830
  };
10678
- const hasMemberCallForRoot = (node, rootName) => {
10831
+ const hasMemberCallForRoot = (node, rootName, scopes) => {
10832
+ const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
10833
+ if (!rootSymbol) return false;
10679
10834
  let didFindMemberCall = false;
10680
10835
  const visit = (current) => {
10681
10836
  if (didFindMemberCall) return;
@@ -10691,7 +10846,7 @@ const hasMemberCallForRoot = (node, rootName) => {
10691
10846
  }
10692
10847
  chainObject = unwrapExpression$3(chainObject.object);
10693
10848
  }
10694
- if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
10849
+ if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
10695
10850
  didFindMemberCall = true;
10696
10851
  return;
10697
10852
  }
@@ -10709,9 +10864,11 @@ const hasMemberCallForRoot = (node, rootName) => {
10709
10864
  visit(node);
10710
10865
  return didFindMemberCall;
10711
10866
  };
10712
- const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
10713
- if ([...captureKeys].filter((captureKey) => captureKey.startsWith("props.")).length < 2 || declaredKeys.has("props")) return;
10714
- if (hasMemberCallForRoot(callback, "props")) captureKeys.add("props");
10867
+ const addAggregatePropsDependency = (captureKeys, declaredKeys, callback, scopes) => {
10868
+ const propsCaptureKeys = [...captureKeys].filter((captureKey) => captureKey.startsWith("props."));
10869
+ if (propsCaptureKeys.length < 2 || declaredKeys.has("props")) return;
10870
+ if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
10871
+ if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
10715
10872
  };
10716
10873
  const exhaustiveDeps = defineRule({
10717
10874
  id: "exhaustive-deps",
@@ -10766,7 +10923,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10766
10923
  };
10767
10924
  const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
10768
10925
  return { CallExpression(node) {
10769
- const hookName = getHookName(node.callee);
10926
+ const hookName = getHookName(node.callee, context.scopes);
10770
10927
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
10771
10928
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10772
10929
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
@@ -10823,7 +10980,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10823
10980
  if (outerAssignments.length > 0) return;
10824
10981
  const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
10825
10982
  const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
10826
- if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
10983
+ if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
10827
10984
  node: callbackToAnalyze,
10828
10985
  message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
10829
10986
  });
@@ -10922,7 +11079,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10922
11079
  const fullChain = stringifyMemberChain(stripped);
10923
11080
  if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
10924
11081
  const refSymbol = context.scopes.symbolFor(stripped.object);
10925
- if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
11082
+ if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
10926
11083
  if (!didReportRefCurrentDep) {
10927
11084
  context.report({
10928
11085
  node: elementNode,
@@ -10960,7 +11117,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10960
11117
  declaredKeys.add(key);
10961
11118
  declaredKeyToReportNode.set(key, elementNode);
10962
11119
  }
10963
- addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
11120
+ addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
10964
11121
  const missingCaptureKeys = [];
10965
11122
  for (const captureKey of captureKeys) {
10966
11123
  let isCoveredByDeclared = false;
@@ -11046,7 +11203,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
11046
11203
  if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
11047
11204
  if (outerFunctionCapturedNames.has(rootName)) continue;
11048
11205
  const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
11049
- if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) && isExtraEffectDepAllowed(reportNode, context.scopes)) continue;
11206
+ if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
11050
11207
  if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
11051
11208
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
11052
11209
  if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
@@ -20988,6 +21145,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
20988
21145
  continue;
20989
21146
  }
20990
21147
  if (isNodeOfType(statement, "ExportNamedDeclaration")) {
21148
+ if (statement.exportKind === "type") continue;
20991
21149
  const declaration = statement.declaration;
20992
21150
  if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
20993
21151
  recordVariableDeclaration(declaration);
@@ -21002,6 +21160,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
21002
21160
  }
21003
21161
  for (const specifier of statement.specifiers ?? []) {
21004
21162
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21163
+ if (specifier.exportKind === "type") continue;
21005
21164
  const local = specifier.local;
21006
21165
  const exported = specifier.exported;
21007
21166
  if (!isNodeOfType(local, "Identifier")) continue;
@@ -21050,25 +21209,37 @@ const resolveImportedExportName = (importSpecifier) => {
21050
21209
  if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
21051
21210
  return null;
21052
21211
  };
21053
- const findReExportSourcesForName = (programRoot, exportedName) => {
21212
+ const findReExportTargetsForName = (programRoot, exportedName) => {
21054
21213
  if (!isNodeOfType(programRoot, "Program")) return [];
21055
- const exportAllSources = [];
21214
+ const exportAllTargets = [];
21056
21215
  for (const statement of programRoot.body ?? []) {
21057
21216
  if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
21217
+ if (statement.exportKind === "type") continue;
21058
21218
  const sourceValue = statement.source.value;
21059
21219
  if (typeof sourceValue !== "string") continue;
21060
21220
  for (const specifier of statement.specifiers ?? []) {
21061
21221
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21222
+ if (specifier.exportKind === "type") continue;
21062
21223
  const exported = specifier.exported;
21063
- if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
21224
+ if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
21225
+ const local = specifier.local;
21226
+ const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
21227
+ if (importedName) return [{
21228
+ importedName,
21229
+ source: sourceValue
21230
+ }];
21064
21231
  }
21065
21232
  }
21066
21233
  if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
21234
+ if (statement.exportKind === "type" || statement.exported) continue;
21067
21235
  const sourceValue = statement.source.value;
21068
- if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
21236
+ if (typeof sourceValue === "string") exportAllTargets.push({
21237
+ importedName: exportedName,
21238
+ source: sourceValue
21239
+ });
21069
21240
  }
21070
21241
  }
21071
- return exportAllSources;
21242
+ return exportAllTargets;
21072
21243
  };
21073
21244
  //#endregion
21074
21245
  //#region src/plugin/utils/attach-parent-references.ts
@@ -21157,139 +21328,6 @@ const parseSourceFile = (absoluteFilePath) => {
21157
21328
  return parsedProgram;
21158
21329
  };
21159
21330
  //#endregion
21160
- //#region src/plugin/utils/is-barrel-index-module.ts
21161
- const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
21162
- const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
21163
- const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
21164
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
21165
- const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
21166
- const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
21167
- const createNonBarrelInfo = () => ({
21168
- isBarrel: false,
21169
- exportsByName: /* @__PURE__ */ new Map(),
21170
- starExportSources: []
21171
- });
21172
- const addImportedBinding = (importedBindings, binding) => {
21173
- importedBindings.set(binding.localName, {
21174
- ...binding,
21175
- didExport: false
21176
- });
21177
- };
21178
- const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
21179
- for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
21180
- localName: specifier.exportedName,
21181
- importedName: specifier.localName,
21182
- source,
21183
- isTypeOnly: specifier.isTypeOnly
21184
- });
21185
- };
21186
- const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
21187
- const trimmedImportClause = importClause.trim();
21188
- const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
21189
- if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
21190
- localName: namespaceMatch[1],
21191
- importedName: "*",
21192
- source,
21193
- isTypeOnly: declarationIsTypeOnly
21194
- });
21195
- const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
21196
- if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
21197
- const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
21198
- if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
21199
- localName: defaultImportName,
21200
- importedName: "default",
21201
- source,
21202
- isTypeOnly: declarationIsTypeOnly
21203
- });
21204
- };
21205
- const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
21206
- let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
21207
- collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
21208
- return "";
21209
- });
21210
- withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
21211
- const isTypeOnly = Boolean(typeKeyword);
21212
- if (namespaceExportName) {
21213
- exportsByName.set(namespaceExportName, {
21214
- exportedName: namespaceExportName,
21215
- importedName: "*",
21216
- source,
21217
- isTypeOnly
21218
- });
21219
- return "";
21220
- }
21221
- if (specifiersText) {
21222
- for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
21223
- exportedName: specifier.exportedName,
21224
- importedName: specifier.localName,
21225
- source,
21226
- isTypeOnly: specifier.isTypeOnly
21227
- });
21228
- return "";
21229
- }
21230
- starExportSources.push(source);
21231
- return "";
21232
- });
21233
- withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
21234
- for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
21235
- const importedBinding = importedBindings.get(specifier.localName);
21236
- if (!importedBinding) return _match;
21237
- importedBinding.didExport = true;
21238
- exportsByName.set(specifier.exportedName, {
21239
- exportedName: specifier.exportedName,
21240
- importedName: importedBinding.importedName,
21241
- source: importedBinding.source,
21242
- isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
21243
- });
21244
- }
21245
- return "";
21246
- });
21247
- return withoutKnownDeclarations;
21248
- };
21249
- const hasUnexportedRuntimeImport = (importedBindings) => {
21250
- for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
21251
- return false;
21252
- };
21253
- const classifyBarrelModule = (sourceText) => {
21254
- const strippedSource = stripJsComments(sourceText).trim();
21255
- if (!strippedSource) return createNonBarrelInfo();
21256
- const importedBindings = /* @__PURE__ */ new Map();
21257
- const exportsByName = /* @__PURE__ */ new Map();
21258
- const starExportSources = [];
21259
- if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
21260
- return {
21261
- isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
21262
- exportsByName,
21263
- starExportSources
21264
- };
21265
- };
21266
- const getBarrelIndexModuleInfo = (filePath) => {
21267
- if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
21268
- recordContentProbe(filePath);
21269
- let fileStat;
21270
- try {
21271
- fileStat = fs.statSync(filePath);
21272
- } catch {
21273
- fileStat = null;
21274
- }
21275
- const cachedResult = barrelIndexModuleInfoCache.get(filePath);
21276
- if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
21277
- if (fileStat === null) return createNonBarrelInfo();
21278
- let moduleInfo = createNonBarrelInfo();
21279
- try {
21280
- moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
21281
- } catch {
21282
- moduleInfo = createNonBarrelInfo();
21283
- }
21284
- barrelIndexModuleInfoCache.set(filePath, {
21285
- mtimeMs: fileStat.mtimeMs,
21286
- size: fileStat.size,
21287
- moduleInfo
21288
- });
21289
- return moduleInfo;
21290
- };
21291
- const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
21292
- //#endregion
21293
21331
  //#region src/plugin/utils/resolve-relative-import-path.ts
21294
21332
  const MODULE_FILE_EXTENSIONS = [
21295
21333
  ".ts",
@@ -21406,35 +21444,6 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
21406
21444
  };
21407
21445
  const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
21408
21446
  //#endregion
21409
- //#region src/plugin/utils/resolve-barrel-export-file-path.ts
21410
- const getUniqueFilePath = (filePaths) => {
21411
- const uniqueFilePaths = new Set(filePaths);
21412
- if (uniqueFilePaths.size !== 1) return null;
21413
- const [filePath] = uniqueFilePaths;
21414
- return filePath ?? null;
21415
- };
21416
- const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
21417
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
21418
- if (!resolvedTargetPath) return null;
21419
- const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
21420
- if (nestedTargetPath) return nestedTargetPath;
21421
- return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
21422
- };
21423
- const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
21424
- if (visitedFilePaths.has(barrelFilePath)) return null;
21425
- visitedFilePaths.add(barrelFilePath);
21426
- const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
21427
- if (!moduleInfo.isBarrel) return null;
21428
- const target = moduleInfo.exportsByName.get(exportedName);
21429
- if (target) {
21430
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
21431
- if (!resolvedTargetPath) return null;
21432
- return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
21433
- }
21434
- if (exportedName === "default") return null;
21435
- return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
21436
- };
21437
- //#endregion
21438
21447
  //#region src/plugin/utils/resolve-tsconfig-alias.ts
21439
21448
  const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
21440
21449
  const isObjectRecord = (value) => typeof value === "object" && value !== null;
@@ -21613,25 +21622,29 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
21613
21622
  };
21614
21623
  //#endregion
21615
21624
  //#region src/plugin/utils/resolve-module-path.ts
21616
- const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
21625
+ const resolveModulePath = (fromFilename, source) => {
21626
+ if (path.isAbsolute(source)) return null;
21627
+ return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
21628
+ };
21617
21629
  //#endregion
21618
21630
  //#region src/plugin/utils/resolve-cross-file-function-export.ts
21619
21631
  const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
21620
21632
  if (visitedFilePaths.size >= 4) return null;
21621
21633
  if (visitedFilePaths.has(filePath)) return null;
21622
21634
  visitedFilePaths.add(filePath);
21623
- const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
21624
- const programRoot = parseSourceFile(actualFilePath);
21635
+ const programRoot = parseSourceFile(filePath);
21625
21636
  if (!programRoot) return null;
21626
21637
  const exported = findExportedFunctionBody(programRoot, exportedName);
21627
21638
  if (exported) return exported;
21628
- for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
21629
- const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
21639
+ const resolvedCandidates = /* @__PURE__ */ new Set();
21640
+ for (const target of findReExportTargetsForName(programRoot, exportedName)) {
21641
+ const nextFilePath = resolveModulePath(filePath, target.source);
21630
21642
  if (!nextFilePath) continue;
21631
- const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
21632
- if (resolved) return resolved;
21643
+ const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
21644
+ if (resolved) resolvedCandidates.add(resolved);
21633
21645
  }
21634
- return null;
21646
+ if (resolvedCandidates.size !== 1) return null;
21647
+ return resolvedCandidates.values().next().value ?? null;
21635
21648
  };
21636
21649
  const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
21637
21650
  const resolvedFilePath = resolveModulePath(fromFilename, source);
@@ -22416,6 +22429,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
22416
22429
  const importDeclaration = declarationNode.parent;
22417
22430
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22418
22431
  }));
22432
+ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
22433
+ if (def.type !== "ImportBinding") return false;
22434
+ const declarationNode = def.node;
22435
+ if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
22436
+ const importDeclaration = declarationNode.parent;
22437
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22438
+ }));
22439
+ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
22440
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
22441
+ const callee = stripParenExpression(declarator.init.callee);
22442
+ if (isNodeOfType(callee, "Identifier")) {
22443
+ const reference = getRef(analysis, callee);
22444
+ if (!reference?.resolved) return callee.name === hookName;
22445
+ return isReactNamedImportReference(reference, hookName);
22446
+ }
22447
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
22448
+ const namespaceReference = getRef(analysis, callee.object);
22449
+ if (!namespaceReference?.resolved) return callee.object.name === "React";
22450
+ return isReactNamespaceImportReference(namespaceReference);
22451
+ };
22419
22452
  const isHookCallee$1 = (analysis, node, hookName) => {
22420
22453
  if (!node) return false;
22421
22454
  if (isNodeOfType(node, "Identifier")) {
@@ -22814,6 +22847,46 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
22814
22847
  "parseInt"
22815
22848
  ]);
22816
22849
  const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22850
+ const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22851
+ "isRawJSON",
22852
+ "parse",
22853
+ "rawJSON",
22854
+ "stringify"
22855
+ ])], ["Math", new Set([
22856
+ "abs",
22857
+ "acos",
22858
+ "acosh",
22859
+ "asin",
22860
+ "asinh",
22861
+ "atan",
22862
+ "atan2",
22863
+ "atanh",
22864
+ "cbrt",
22865
+ "ceil",
22866
+ "clz32",
22867
+ "cos",
22868
+ "cosh",
22869
+ "exp",
22870
+ "floor",
22871
+ "fround",
22872
+ "hypot",
22873
+ "imul",
22874
+ "log",
22875
+ "log10",
22876
+ "log1p",
22877
+ "log2",
22878
+ "max",
22879
+ "min",
22880
+ "pow",
22881
+ "round",
22882
+ "sign",
22883
+ "sin",
22884
+ "sinh",
22885
+ "sqrt",
22886
+ "tan",
22887
+ "tanh",
22888
+ "trunc"
22889
+ ])]]);
22817
22890
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22818
22891
  "concat",
22819
22892
  "filter",
@@ -22860,6 +22933,42 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
22860
22933
  }
22861
22934
  return parameter;
22862
22935
  };
22936
+ const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
22937
+ const isModuleFunction = (functionNode) => {
22938
+ let ancestor = functionNode.parent;
22939
+ while (ancestor) {
22940
+ if (isFunctionLike$1(ancestor)) return false;
22941
+ if (isNodeOfType(ancestor, "Program")) return true;
22942
+ ancestor = ancestor.parent;
22943
+ }
22944
+ return false;
22945
+ };
22946
+ const getFunctionBindingNames = (functionNode) => {
22947
+ const names = /* @__PURE__ */ new Set();
22948
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
22949
+ const parent = functionNode.parent;
22950
+ if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
22951
+ return names;
22952
+ };
22953
+ const collectModuleBindingNames = (functionNode) => {
22954
+ let program = functionNode.parent;
22955
+ while (program && !isNodeOfType(program, "Program")) program = program.parent;
22956
+ const bindingNames = /* @__PURE__ */ new Set();
22957
+ if (!program || !isNodeOfType(program, "Program")) return bindingNames;
22958
+ for (const statement of program.body ?? []) {
22959
+ if (isNodeOfType(statement, "ImportDeclaration")) {
22960
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
22961
+ continue;
22962
+ }
22963
+ const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
22964
+ if (isNodeOfType(declaration, "VariableDeclaration")) {
22965
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
22966
+ continue;
22967
+ }
22968
+ if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
22969
+ }
22970
+ return bindingNames;
22971
+ };
22863
22972
  const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
22864
22973
  const substitutions = /* @__PURE__ */ new Map();
22865
22974
  const parameters = getFunctionParameters(functionNode);
@@ -22990,7 +23099,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
22990
23099
  for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
22991
23100
  return introducedBindings;
22992
23101
  };
22993
- const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
23102
+ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
22994
23103
  const effectFunction = getEffectFn(analysis, effectNode);
22995
23104
  if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
22996
23105
  const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
@@ -22999,7 +23108,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22999
23108
  invocation: null,
23000
23109
  isDeferred: false,
23001
23110
  introducedBindings: /* @__PURE__ */ new Set(),
23002
- substitutions: /* @__PURE__ */ new Map()
23111
+ substitutions: /* @__PURE__ */ new Map(),
23112
+ currentFilename
23003
23113
  };
23004
23114
  const frames = [rootFrame];
23005
23115
  walkAst(effectFunction, (child) => {
@@ -23020,7 +23130,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
23020
23130
  invocation: child,
23021
23131
  isDeferred,
23022
23132
  introducedBindings,
23023
- substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
23133
+ substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
23134
+ currentFilename
23024
23135
  });
23025
23136
  };
23026
23137
  if (isFunctionLike$1(callee)) {
@@ -23073,6 +23184,174 @@ const isOpaqueHookCall = (callExpression) => {
23073
23184
  const calleeName = getCallCalleeName$1(callExpression);
23074
23185
  return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
23075
23186
  };
23187
+ const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
23188
+ let canContinue = true;
23189
+ for (const statement of statements) {
23190
+ if (!canContinue) {
23191
+ if (!isNodeOfType(statement, "EmptyStatement")) return {
23192
+ canContinue: false,
23193
+ isValid: false
23194
+ };
23195
+ continue;
23196
+ }
23197
+ if (isNodeOfType(statement, "EmptyStatement")) continue;
23198
+ if (isNodeOfType(statement, "ReturnStatement")) {
23199
+ if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
23200
+ canContinue: false,
23201
+ isValid: false
23202
+ };
23203
+ canContinue = false;
23204
+ continue;
23205
+ }
23206
+ if (isNodeOfType(statement, "BlockStatement")) {
23207
+ const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
23208
+ if (!blockSummary.isValid) return blockSummary;
23209
+ canContinue = blockSummary.canContinue;
23210
+ continue;
23211
+ }
23212
+ if (isNodeOfType(statement, "IfStatement")) {
23213
+ if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
23214
+ canContinue: false,
23215
+ isValid: false
23216
+ };
23217
+ const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
23218
+ if (!consequentSummary.isValid) return consequentSummary;
23219
+ const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
23220
+ canContinue: true,
23221
+ isValid: true
23222
+ };
23223
+ if (!alternateSummary.isValid) return alternateSummary;
23224
+ canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
23225
+ continue;
23226
+ }
23227
+ return {
23228
+ canContinue: false,
23229
+ isValid: false
23230
+ };
23231
+ }
23232
+ return {
23233
+ canContinue,
23234
+ isValid: true
23235
+ };
23236
+ };
23237
+ const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
23238
+ const node = stripParenExpression(expression);
23239
+ if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
23240
+ if (isNodeOfType(node, "Identifier")) {
23241
+ const parameterIndex = environment.parameterIndices.get(node.name);
23242
+ if (parameterIndex !== void 0) {
23243
+ if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
23244
+ return true;
23245
+ }
23246
+ return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
23247
+ }
23248
+ if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
23249
+ if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
23250
+ if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
23251
+ if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
23252
+ if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
23253
+ return analyzeHelperExpression(property.value, environment, usedParameterIndices);
23254
+ });
23255
+ if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
23256
+ if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23257
+ if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
23258
+ if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
23259
+ if (isNodeOfType(node, "MemberExpression")) {
23260
+ if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
23261
+ return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
23262
+ }
23263
+ if (isFunctionLike$1(node)) {
23264
+ if (isAsyncOrGeneratorFunction(node)) return false;
23265
+ const callbackParameterIndices = new Map(environment.parameterIndices);
23266
+ for (const parameter of getFunctionParameters(node)) {
23267
+ if (!isNodeOfType(parameter, "Identifier")) return false;
23268
+ callbackParameterIndices.set(parameter.name, null);
23269
+ }
23270
+ const callbackEnvironment = {
23271
+ parameterIndices: callbackParameterIndices,
23272
+ recursiveNames: environment.recursiveNames,
23273
+ shadowedGlobalNames: environment.shadowedGlobalNames
23274
+ };
23275
+ if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
23276
+ const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23277
+ return callbackSummary.isValid && !callbackSummary.canContinue;
23278
+ }
23279
+ if (isNodeOfType(node, "CallExpression")) {
23280
+ const callee = stripParenExpression(node.callee);
23281
+ const calleeRoot = getMemberRoot(callee);
23282
+ const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && !environment.parameterIndices.has(callee.name) && !environment.recursiveNames.has(callee.name) && !environment.shadowedGlobalNames.has(callee.name);
23283
+ const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
23284
+ const namespaceMemberName = getStaticMemberName(callee);
23285
+ const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
23286
+ const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23287
+ if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
23288
+ if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
23289
+ return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23290
+ }
23291
+ if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23292
+ return false;
23293
+ };
23294
+ const helperSummaryCache = /* @__PURE__ */ new WeakMap();
23295
+ const summarizeHelperReturn = (functionNode) => {
23296
+ if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
23297
+ if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
23298
+ if (isAsyncOrGeneratorFunction(functionNode)) {
23299
+ helperSummaryCache.set(functionNode, null);
23300
+ return null;
23301
+ }
23302
+ const parameterIndices = /* @__PURE__ */ new Map();
23303
+ const parameters = getFunctionParameters(functionNode);
23304
+ for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
23305
+ const parameter = parameters[parameterIndex];
23306
+ if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
23307
+ helperSummaryCache.set(functionNode, null);
23308
+ return null;
23309
+ }
23310
+ parameterIndices.set(parameter.name, parameterIndex);
23311
+ }
23312
+ const environment = {
23313
+ parameterIndices,
23314
+ recursiveNames: getFunctionBindingNames(functionNode),
23315
+ shadowedGlobalNames: collectModuleBindingNames(functionNode)
23316
+ };
23317
+ const usedParameterIndices = /* @__PURE__ */ new Set();
23318
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) {
23319
+ if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
23320
+ helperSummaryCache.set(functionNode, null);
23321
+ return null;
23322
+ }
23323
+ } else {
23324
+ const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
23325
+ if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
23326
+ helperSummaryCache.set(functionNode, null);
23327
+ return null;
23328
+ }
23329
+ }
23330
+ const summary = { usedParameterIndices };
23331
+ helperSummaryCache.set(functionNode, summary);
23332
+ return summary;
23333
+ };
23334
+ const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
23335
+ if (!isNodeOfType(callee, "Identifier")) return null;
23336
+ const reference = getRef(analysis, callee);
23337
+ if (!reference?.resolved) return null;
23338
+ const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
23339
+ if (importDefinition) {
23340
+ if (!currentFilename) return null;
23341
+ const specifier = importDefinition.node;
23342
+ if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
23343
+ const importDeclaration = specifier.parent;
23344
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
23345
+ if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
23346
+ const source = importDeclaration.source?.value;
23347
+ if (typeof source !== "string") return null;
23348
+ const exportedName = resolveImportedExportName(specifier);
23349
+ if (!exportedName) return null;
23350
+ return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
23351
+ }
23352
+ const callable = resolveWrappedCallable(analysis, callee);
23353
+ return callable && isModuleFunction(callable) ? callable : null;
23354
+ };
23076
23355
  const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
23077
23356
  const definitionNode = definition.node;
23078
23357
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
@@ -23158,38 +23437,55 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23158
23437
  const calleeRoot = getMemberRoot(callee);
23159
23438
  const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
23160
23439
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23161
- if (isPureMemberTransform) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
23162
- for (const argument of node.arguments ?? []) {
23163
- if (isFunctionLike$1(argument)) continue;
23164
- mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23165
- }
23166
- if (isPureGlobalCall || isPureMemberTransform) return evidence;
23167
- if (isNodeOfType(callee, "MemberExpression")) {
23168
- evidence.hasUnknownSource = true;
23440
+ if (isPureGlobalCall || isPureMemberTransform) {
23441
+ if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
23442
+ for (const argument of node.arguments ?? []) {
23443
+ if (isFunctionLike$1(argument)) continue;
23444
+ mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23445
+ }
23169
23446
  return evidence;
23170
23447
  }
23171
23448
  if (remainingCallFrames <= 0) {
23172
23449
  evidence.hasUnknownSource = true;
23173
23450
  return evidence;
23174
23451
  }
23175
- const callable = resolveWrappedCallable(analysis, callee);
23176
- if (!callable || callable.async === true || functionInvokesItself(analysis, callable)) {
23177
- evidence.hasUnknownSource = true;
23452
+ const localHelperFunction = resolveWrappedCallable(analysis, callee);
23453
+ if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
23454
+ if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
23455
+ evidence.hasUnknownSource = true;
23456
+ return evidence;
23457
+ }
23458
+ const localHelperFrame = {
23459
+ functionNode: localHelperFunction,
23460
+ invocation: node,
23461
+ isDeferred: false,
23462
+ introducedBindings: /* @__PURE__ */ new Set(),
23463
+ substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
23464
+ currentFilename: frame.currentFilename
23465
+ };
23466
+ const returnedExpressions = getReturnedExpressions(localHelperFunction);
23467
+ if (returnedExpressions.length === 0) {
23468
+ evidence.hasUnknownSource = true;
23469
+ return evidence;
23470
+ }
23471
+ for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
23178
23472
  return evidence;
23179
23473
  }
23180
- const valueFrame = {
23181
- functionNode: callable,
23182
- invocation: node,
23183
- isDeferred: false,
23184
- introducedBindings: /* @__PURE__ */ new Set(),
23185
- substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
23186
- };
23187
- const returnedExpressions = getReturnedExpressions(callable);
23188
- if (returnedExpressions.length === 0) {
23474
+ const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
23475
+ const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
23476
+ if (!helperSummary) {
23189
23477
  evidence.hasUnknownSource = true;
23190
23478
  return evidence;
23191
23479
  }
23192
- for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, valueFrame, remainingCallFrames - 1, new Set(visitedBindings)));
23480
+ const argumentsForHelper = node.arguments ?? [];
23481
+ for (const parameterIndex of helperSummary.usedParameterIndices) {
23482
+ const argument = argumentsForHelper[parameterIndex];
23483
+ if (!argument) {
23484
+ evidence.hasUnknownSource = true;
23485
+ return evidence;
23486
+ }
23487
+ mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
23488
+ }
23193
23489
  return evidence;
23194
23490
  }
23195
23491
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
@@ -23207,6 +23503,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23207
23503
  }
23208
23504
  return evidence;
23209
23505
  };
23506
+ const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
23507
+ const evidence = collectValueEvidence(analysis, expression, {
23508
+ functionNode: componentFunction,
23509
+ invocation: null,
23510
+ isDeferred: false,
23511
+ introducedBindings: /* @__PURE__ */ new Set(),
23512
+ substitutions: /* @__PURE__ */ new Map(),
23513
+ currentFilename
23514
+ }, 1);
23515
+ return {
23516
+ sourceReferences: evidence.sourceReferences,
23517
+ isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
23518
+ };
23519
+ };
23210
23520
  const findStateSetterReference = (analysis, callExpression) => {
23211
23521
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
23212
23522
  const callee = stripParenExpression(callExpression.callee);
@@ -23269,8 +23579,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
23269
23579
  }
23270
23580
  return false;
23271
23581
  };
23272
- const collectEffectStateWriteFacts = (analysis, effectNode) => {
23273
- const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
23582
+ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
23583
+ const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
23274
23584
  if (frames.length === 0) return [];
23275
23585
  const effectHasCleanup = hasCleanup(analysis, effectNode);
23276
23586
  const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
@@ -23290,7 +23600,8 @@ const collectEffectStateWriteFacts = (analysis, effectNode) => {
23290
23600
  invocation: callExpression,
23291
23601
  isDeferred: frame.isDeferred,
23292
23602
  introducedBindings: collectIntroducedBindings(analysis, writtenValue),
23293
- substitutions: /* @__PURE__ */ new Map()
23603
+ substitutions: /* @__PURE__ */ new Map(),
23604
+ currentFilename
23294
23605
  };
23295
23606
  valueEvidence = emptyEvidence();
23296
23607
  const returnedExpressions = getReturnedExpressions(writtenValue);
@@ -23339,7 +23650,7 @@ const noAdjustStateOnPropChange = defineRule({
23339
23650
  const dependencyReferences = getEffectDepsRefs(analysis, node);
23340
23651
  if (!dependencyReferences) return;
23341
23652
  if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
23342
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
23653
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
23343
23654
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
23344
23655
  context.report({
23345
23656
  node: fact.callExpression,
@@ -24600,6 +24911,139 @@ const createRelativeImportSource = (filename, targetFilePath) => {
24600
24911
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
24601
24912
  };
24602
24913
  //#endregion
24914
+ //#region src/plugin/utils/is-barrel-index-module.ts
24915
+ const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
24916
+ const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24917
+ const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24918
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24919
+ const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
24920
+ const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
24921
+ const createNonBarrelInfo = () => ({
24922
+ isBarrel: false,
24923
+ exportsByName: /* @__PURE__ */ new Map(),
24924
+ starExportSources: []
24925
+ });
24926
+ const addImportedBinding = (importedBindings, binding) => {
24927
+ importedBindings.set(binding.localName, {
24928
+ ...binding,
24929
+ didExport: false
24930
+ });
24931
+ };
24932
+ const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
24933
+ for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
24934
+ localName: specifier.exportedName,
24935
+ importedName: specifier.localName,
24936
+ source,
24937
+ isTypeOnly: specifier.isTypeOnly
24938
+ });
24939
+ };
24940
+ const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
24941
+ const trimmedImportClause = importClause.trim();
24942
+ const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
24943
+ if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
24944
+ localName: namespaceMatch[1],
24945
+ importedName: "*",
24946
+ source,
24947
+ isTypeOnly: declarationIsTypeOnly
24948
+ });
24949
+ const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
24950
+ if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
24951
+ const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
24952
+ if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
24953
+ localName: defaultImportName,
24954
+ importedName: "default",
24955
+ source,
24956
+ isTypeOnly: declarationIsTypeOnly
24957
+ });
24958
+ };
24959
+ const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
24960
+ let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
24961
+ collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
24962
+ return "";
24963
+ });
24964
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
24965
+ const isTypeOnly = Boolean(typeKeyword);
24966
+ if (namespaceExportName) {
24967
+ exportsByName.set(namespaceExportName, {
24968
+ exportedName: namespaceExportName,
24969
+ importedName: "*",
24970
+ source,
24971
+ isTypeOnly
24972
+ });
24973
+ return "";
24974
+ }
24975
+ if (specifiersText) {
24976
+ for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
24977
+ exportedName: specifier.exportedName,
24978
+ importedName: specifier.localName,
24979
+ source,
24980
+ isTypeOnly: specifier.isTypeOnly
24981
+ });
24982
+ return "";
24983
+ }
24984
+ starExportSources.push(source);
24985
+ return "";
24986
+ });
24987
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
24988
+ for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
24989
+ const importedBinding = importedBindings.get(specifier.localName);
24990
+ if (!importedBinding) return _match;
24991
+ importedBinding.didExport = true;
24992
+ exportsByName.set(specifier.exportedName, {
24993
+ exportedName: specifier.exportedName,
24994
+ importedName: importedBinding.importedName,
24995
+ source: importedBinding.source,
24996
+ isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
24997
+ });
24998
+ }
24999
+ return "";
25000
+ });
25001
+ return withoutKnownDeclarations;
25002
+ };
25003
+ const hasUnexportedRuntimeImport = (importedBindings) => {
25004
+ for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
25005
+ return false;
25006
+ };
25007
+ const classifyBarrelModule = (sourceText) => {
25008
+ const strippedSource = stripJsComments(sourceText).trim();
25009
+ if (!strippedSource) return createNonBarrelInfo();
25010
+ const importedBindings = /* @__PURE__ */ new Map();
25011
+ const exportsByName = /* @__PURE__ */ new Map();
25012
+ const starExportSources = [];
25013
+ if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
25014
+ return {
25015
+ isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
25016
+ exportsByName,
25017
+ starExportSources
25018
+ };
25019
+ };
25020
+ const getBarrelIndexModuleInfo = (filePath) => {
25021
+ if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
25022
+ recordContentProbe(filePath);
25023
+ let fileStat;
25024
+ try {
25025
+ fileStat = fs.statSync(filePath);
25026
+ } catch {
25027
+ fileStat = null;
25028
+ }
25029
+ const cachedResult = barrelIndexModuleInfoCache.get(filePath);
25030
+ if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
25031
+ if (fileStat === null) return createNonBarrelInfo();
25032
+ let moduleInfo = createNonBarrelInfo();
25033
+ try {
25034
+ moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
25035
+ } catch {
25036
+ moduleInfo = createNonBarrelInfo();
25037
+ }
25038
+ barrelIndexModuleInfoCache.set(filePath, {
25039
+ mtimeMs: fileStat.mtimeMs,
25040
+ size: fileStat.size,
25041
+ moduleInfo
25042
+ });
25043
+ return moduleInfo;
25044
+ };
25045
+ const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
25046
+ //#endregion
24603
25047
  //#region src/react-native-dependency-names.ts
24604
25048
  const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
24605
25049
  "expo",
@@ -24802,6 +25246,35 @@ const classifyReactNativeFileTarget = (context) => {
24802
25246
  };
24803
25247
  const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
24804
25248
  //#endregion
25249
+ //#region src/plugin/utils/resolve-barrel-export-file-path.ts
25250
+ const getUniqueFilePath = (filePaths) => {
25251
+ const uniqueFilePaths = new Set(filePaths);
25252
+ if (uniqueFilePaths.size !== 1) return null;
25253
+ const [filePath] = uniqueFilePaths;
25254
+ return filePath ?? null;
25255
+ };
25256
+ const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
25257
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
25258
+ if (!resolvedTargetPath) return null;
25259
+ const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
25260
+ if (nestedTargetPath) return nestedTargetPath;
25261
+ return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
25262
+ };
25263
+ const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
25264
+ if (visitedFilePaths.has(barrelFilePath)) return null;
25265
+ visitedFilePaths.add(barrelFilePath);
25266
+ const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
25267
+ if (!moduleInfo.isBarrel) return null;
25268
+ const target = moduleInfo.exportsByName.get(exportedName);
25269
+ if (target) {
25270
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
25271
+ if (!resolvedTargetPath) return null;
25272
+ return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
25273
+ }
25274
+ if (exportedName === "default") return null;
25275
+ return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
25276
+ };
25277
+ //#endregion
24805
25278
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
24806
25279
  const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
24807
25280
  const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
@@ -26096,57 +26569,6 @@ const noDefaultProps = defineRule({
26096
26569
  } })
26097
26570
  });
26098
26571
  //#endregion
26099
- //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26100
- const getStateName$1 = (stateDeclarator) => {
26101
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26102
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26103
- const stateBinding = stateDeclarator.id.elements?.[0];
26104
- if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26105
- const setterBinding = stateDeclarator.id.elements?.[1];
26106
- if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26107
- if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26108
- return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26109
- };
26110
- const noDerivedState = defineRule({
26111
- id: "no-derived-state",
26112
- title: "Derived value copied into state",
26113
- severity: "warn",
26114
- tags: ["test-noise"],
26115
- recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into useState through a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
26116
- create: (context) => ({ CallExpression(node) {
26117
- if (!isUseEffect(node)) return;
26118
- const analysis = getProgramAnalysis(node);
26119
- if (!analysis) return;
26120
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
26121
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26122
- const stateName = getStateName$1(fact.stateDeclarator);
26123
- context.report({
26124
- node: fact.callExpression,
26125
- message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26126
- });
26127
- }
26128
- } })
26129
- });
26130
- //#endregion
26131
- //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26132
- const noDerivedStateEffect = defineRule({
26133
- id: "no-derived-state-effect",
26134
- title: "Derived state stored in an effect",
26135
- severity: "warn",
26136
- tags: ["test-noise"],
26137
- recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
26138
- create: (context) => ({ CallExpression(node) {
26139
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26140
- const analysis = getProgramAnalysis(node);
26141
- if (!analysis) return;
26142
- if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26143
- context.report({
26144
- node,
26145
- message: "You pay an extra render for state you can derive from other values."
26146
- });
26147
- } })
26148
- });
26149
- //#endregion
26150
26572
  //#region src/plugin/utils/is-component-function.ts
26151
26573
  const isFunctionAssignedToComponent = (functionNode) => {
26152
26574
  let cursor = functionNode.parent ?? null;
@@ -26280,6 +26702,236 @@ const createComponentPropStackTracker = (callbacks) => {
26280
26702
  };
26281
26703
  };
26282
26704
  //#endregion
26705
+ //#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
26706
+ const findReferenceDeclarator = (reference) => {
26707
+ for (const definition of reference.resolved?.defs ?? []) {
26708
+ const definitionNode = definition.node;
26709
+ if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
26710
+ }
26711
+ return null;
26712
+ };
26713
+ const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
26714
+ const node = stripParenExpression(expression);
26715
+ if (!isNodeOfType(node, "Identifier")) return null;
26716
+ const reference = getRef(analysis, node);
26717
+ if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
26718
+ if (isProp(analysis, reference)) {
26719
+ if (isWholePropsObjectReference(analysis, reference)) return null;
26720
+ return { bindingIdentity: reference.resolved };
26721
+ }
26722
+ if (isState(analysis, reference)) {
26723
+ const stateDeclarator = getUseStateDecl(analysis, reference);
26724
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
26725
+ return { bindingIdentity: reference.resolved };
26726
+ }
26727
+ if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
26728
+ const declarator = findReferenceDeclarator(reference);
26729
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
26730
+ const nextVisitedBindings = new Set(visitedBindings);
26731
+ nextVisitedBindings.add(reference.resolved);
26732
+ return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
26733
+ };
26734
+ const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
26735
+ const getDirectBranchStatements = (branch) => {
26736
+ if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
26737
+ return [branch];
26738
+ };
26739
+ const getDirectCallExpression = (statement) => {
26740
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26741
+ const expression = stripParenExpression(statement.expression);
26742
+ return isNodeOfType(expression, "CallExpression") ? expression : null;
26743
+ };
26744
+ const getDirectAssignmentExpression = (statement) => {
26745
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26746
+ const expression = stripParenExpression(statement.expression);
26747
+ return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
26748
+ };
26749
+ const findDirectStateSetterReference = (analysis, callExpression) => {
26750
+ if (!isNodeOfType(callExpression, "CallExpression")) return null;
26751
+ const callee = stripParenExpression(callExpression.callee);
26752
+ if (!isNodeOfType(callee, "Identifier")) return null;
26753
+ const reference = getRef(analysis, callee);
26754
+ return reference && isStateSetter(analysis, reference) ? reference : null;
26755
+ };
26756
+ const getStaticRefCurrentReference = (analysis, expression) => {
26757
+ const node = stripParenExpression(expression);
26758
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
26759
+ const reference = getRef(analysis, node.object);
26760
+ if (!reference) return null;
26761
+ const declarator = findReferenceDeclarator(reference);
26762
+ return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
26763
+ };
26764
+ const getStateTracker = (analysis, expression) => {
26765
+ const node = stripParenExpression(expression);
26766
+ if (!isNodeOfType(node, "Identifier")) return null;
26767
+ const reference = getRef(analysis, node);
26768
+ if (!reference || !isState(analysis, reference)) return null;
26769
+ const declarator = getUseStateDecl(analysis, reference);
26770
+ if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
26771
+ return {
26772
+ kind: "state",
26773
+ declarator
26774
+ };
26775
+ };
26776
+ const getRefTracker = (analysis, expression) => {
26777
+ const reference = getStaticRefCurrentReference(analysis, expression);
26778
+ return reference ? {
26779
+ kind: "ref",
26780
+ reference
26781
+ } : null;
26782
+ };
26783
+ const getTrackerInitializer = (tracker) => {
26784
+ const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
26785
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
26786
+ const initializer = declarator.init.arguments?.[0];
26787
+ return initializer ? initializer : null;
26788
+ };
26789
+ const isTrackerSynchronized = (analysis, guard) => {
26790
+ for (const statement of guard.statements) {
26791
+ if (guard.tracker.kind === "state") {
26792
+ const callExpression = getDirectCallExpression(statement);
26793
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26794
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26795
+ if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
26796
+ const writtenValue = callExpression.arguments?.[0];
26797
+ if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
26798
+ continue;
26799
+ }
26800
+ const assignmentExpression = getDirectAssignmentExpression(statement);
26801
+ if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
26802
+ if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
26803
+ if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
26804
+ }
26805
+ return false;
26806
+ };
26807
+ const parseRenderTrackerGuard = (analysis, statement) => {
26808
+ if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
26809
+ const left = statement.test.left;
26810
+ const right = statement.test.right;
26811
+ const candidates = [{
26812
+ sourceExpression: left,
26813
+ trackerExpression: right
26814
+ }, {
26815
+ sourceExpression: right,
26816
+ trackerExpression: left
26817
+ }];
26818
+ for (const candidate of candidates) {
26819
+ const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
26820
+ if (!source) continue;
26821
+ const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
26822
+ if (!tracker) continue;
26823
+ const trackerInitializer = getTrackerInitializer(tracker);
26824
+ if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
26825
+ const guard = {
26826
+ source,
26827
+ tracker,
26828
+ statements: getDirectBranchStatements(statement.consequent)
26829
+ };
26830
+ return isTrackerSynchronized(analysis, guard) ? guard : null;
26831
+ }
26832
+ return null;
26833
+ };
26834
+ const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
26835
+ if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
26836
+ const references = setterReference.resolved.references.filter((reference) => !reference.init);
26837
+ if (references.length !== 1) return false;
26838
+ return references[0].identifier === callExpression.callee;
26839
+ };
26840
+ const getStateInitializer = (stateDeclarator) => {
26841
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
26842
+ const initializer = stateDeclarator.init.arguments?.[0];
26843
+ return initializer ? initializer : null;
26844
+ };
26845
+ const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
26846
+ if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
26847
+ const componentFunction = componentBody.parent;
26848
+ const facts = [];
26849
+ for (const statement of componentBody.body ?? []) {
26850
+ const guard = parseRenderTrackerGuard(analysis, statement);
26851
+ if (!guard) continue;
26852
+ for (const branchStatement of guard.statements) {
26853
+ const callExpression = getDirectCallExpression(branchStatement);
26854
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26855
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26856
+ if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
26857
+ const stateDeclarator = getUseStateDecl(analysis, setterReference);
26858
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
26859
+ const writtenValue = callExpression.arguments?.[0];
26860
+ const initializer = getStateInitializer(stateDeclarator);
26861
+ if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
26862
+ facts.push({
26863
+ callExpression,
26864
+ stateDeclarator
26865
+ });
26866
+ }
26867
+ }
26868
+ return facts;
26869
+ };
26870
+ //#endregion
26871
+ //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26872
+ const getStateName$1 = (stateDeclarator) => {
26873
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26874
+ if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26875
+ const stateBinding = stateDeclarator.id.elements?.[0];
26876
+ if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26877
+ const setterBinding = stateDeclarator.id.elements?.[1];
26878
+ if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26879
+ if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26880
+ return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26881
+ };
26882
+ const noDerivedState = defineRule({
26883
+ id: "no-derived-state",
26884
+ title: "Derived value copied into state",
26885
+ severity: "warn",
26886
+ tags: ["test-noise"],
26887
+ recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
26888
+ create: (context) => {
26889
+ const reportStateWrite = (callExpression, stateDeclarator) => {
26890
+ const stateName = getStateName$1(stateDeclarator);
26891
+ context.report({
26892
+ node: callExpression,
26893
+ message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26894
+ });
26895
+ };
26896
+ return {
26897
+ ...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
26898
+ if (!componentBody) return;
26899
+ const analysis = getProgramAnalysis(componentBody);
26900
+ if (!analysis) return;
26901
+ for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
26902
+ } }).visitors,
26903
+ CallExpression(node) {
26904
+ if (!isUseEffect(node)) return;
26905
+ const analysis = getProgramAnalysis(node);
26906
+ if (!analysis) return;
26907
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
26908
+ if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26909
+ reportStateWrite(fact.callExpression, fact.stateDeclarator);
26910
+ }
26911
+ }
26912
+ };
26913
+ }
26914
+ });
26915
+ //#endregion
26916
+ //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26917
+ const noDerivedStateEffect = defineRule({
26918
+ id: "no-derived-state-effect",
26919
+ title: "Derived state stored in an effect",
26920
+ severity: "warn",
26921
+ tags: ["test-noise"],
26922
+ recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
26923
+ create: (context) => ({ CallExpression(node) {
26924
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26925
+ const analysis = getProgramAnalysis(node);
26926
+ if (!analysis) return;
26927
+ if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26928
+ context.report({
26929
+ node,
26930
+ message: "You pay an extra render for state you can derive from other values."
26931
+ });
26932
+ } })
26933
+ });
26934
+ //#endregion
26283
26935
  //#region src/plugin/utils/is-initial-only-prop-name.ts
26284
26936
  const isInitialOnlyPropName = (propName) => {
26285
26937
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -29643,28 +30295,26 @@ const noGiantComponent = defineRule({
29643
30295
  const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
29644
30296
  return lineCount > 300 ? lineCount : null;
29645
30297
  };
29646
- const reportOversizedComponent = (nameNode, componentName, lineCount) => {
30298
+ const reportOversizedComponent = (nameNode, componentName) => {
29647
30299
  context.report({
29648
30300
  node: nameNode,
29649
- message: `Component "${componentName}" is ${lineCount} lines long, which is hard to read & change. Split it into a few smaller components.`
30301
+ message: `Component "${componentName}" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.`
29650
30302
  });
29651
30303
  };
29652
30304
  return {
29653
30305
  FunctionDeclaration(node) {
29654
30306
  if (!node.id?.name || !isUppercaseName(node.id.name)) return;
29655
- const lineCount = getOversizedComponentLineCount(node);
29656
- if (lineCount === null) return;
30307
+ if (getOversizedComponentLineCount(node) === null) return;
29657
30308
  if (!functionContainsReactRenderOutput(node, context.scopes)) return;
29658
- reportOversizedComponent(node.id, node.id.name, lineCount);
30309
+ reportOversizedComponent(node.id, node.id.name);
29659
30310
  },
29660
30311
  VariableDeclarator(node) {
29661
30312
  if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
29662
30313
  const functionNode = unwrapReactHocFunction(node.init);
29663
30314
  if (!functionNode) return;
29664
- const lineCount = getOversizedComponentLineCount(functionNode);
29665
- if (lineCount === null) return;
30315
+ if (getOversizedComponentLineCount(functionNode) === null) return;
29666
30316
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
29667
- reportOversizedComponent(node.id, node.id.name, lineCount);
30317
+ reportOversizedComponent(node.id, node.id.name);
29668
30318
  }
29669
30319
  };
29670
30320
  }
@@ -30301,6 +30951,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
30301
30951
  } })
30302
30952
  });
30303
30953
  //#endregion
30954
+ //#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
30955
+ const TIMER_FUNCTION_NAMES = new Set([
30956
+ "cancelAnimationFrame",
30957
+ "clearInterval",
30958
+ "clearTimeout",
30959
+ "queueMicrotask",
30960
+ "requestAnimationFrame",
30961
+ "setInterval",
30962
+ "setTimeout"
30963
+ ]);
30964
+ const STORAGE_MUTATION_METHOD_NAMES = new Set([
30965
+ "clear",
30966
+ "removeItem",
30967
+ "setItem"
30968
+ ]);
30969
+ const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
30970
+ const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
30971
+ const NOTIFICATION_RECEIVER_NAMES = new Set([
30972
+ "message",
30973
+ "notification",
30974
+ "toast"
30975
+ ]);
30976
+ const NOTIFICATION_METHOD_NAMES = new Set([
30977
+ "error",
30978
+ "info",
30979
+ "loading",
30980
+ "open",
30981
+ "show",
30982
+ "success",
30983
+ "warning"
30984
+ ]);
30985
+ const getMemberCall = (node) => {
30986
+ if (!isNodeOfType(node, "CallExpression")) return null;
30987
+ if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
30988
+ if (!isNodeOfType(node.callee.property, "Identifier")) return null;
30989
+ return {
30990
+ methodName: node.callee.property.name,
30991
+ receiver: stripParenExpression(node.callee.object)
30992
+ };
30993
+ };
30994
+ const isNotificationReceiver = (receiver, scopes) => {
30995
+ if (!isNodeOfType(receiver, "Identifier")) return false;
30996
+ if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
30997
+ const symbol = scopes.symbolFor(receiver);
30998
+ if (symbol?.kind === "import") return true;
30999
+ if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
31000
+ const callee = symbol.initializer.callee;
31001
+ return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
31002
+ };
31003
+ const getKnownImpureCall = (callExpression, scopes) => {
31004
+ if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
31005
+ const memberCall = getMemberCall(callExpression);
31006
+ if (!memberCall) return null;
31007
+ const { methodName, receiver } = memberCall;
31008
+ if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
31009
+ if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
31010
+ if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
31011
+ return null;
31012
+ };
31013
+ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
31014
+ let rootIdentifier = null;
31015
+ if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
31016
+ else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
31017
+ if (!rootIdentifier) return null;
31018
+ const updaterScope = scopes.ownScopeFor(updater);
31019
+ if (!updaterScope) return null;
31020
+ const symbol = scopes.symbolFor(rootIdentifier);
31021
+ if (!symbol) return `the external value "${rootIdentifier.name}"`;
31022
+ if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
31023
+ return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
31024
+ };
31025
+ const findImpureUpdaterOperation = (updater, scopes) => {
31026
+ const analysis = getProgramAnalysis(updater);
31027
+ let operation = null;
31028
+ walkAst(updater, (child) => {
31029
+ if (operation) return false;
31030
+ if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
31031
+ if (isNodeOfType(child, "CallExpression")) {
31032
+ if (isNodeOfType(child.callee, "Identifier") && analysis) {
31033
+ const calleeReference = getRef(analysis, child.callee);
31034
+ if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
31035
+ operation = `the nested state update "${child.callee.name}()"`;
31036
+ return false;
31037
+ }
31038
+ }
31039
+ const impureCall = getKnownImpureCall(child, scopes);
31040
+ if (impureCall) {
31041
+ operation = impureCall;
31042
+ return false;
31043
+ }
31044
+ }
31045
+ if (isNodeOfType(child, "AssignmentExpression")) {
31046
+ operation = getExternalAssignmentDescription(child.left, updater, scopes);
31047
+ if (operation) return false;
31048
+ }
31049
+ if (isNodeOfType(child, "UpdateExpression")) {
31050
+ operation = getExternalAssignmentDescription(child.argument, updater, scopes);
31051
+ if (operation) return false;
31052
+ }
31053
+ });
31054
+ return operation;
31055
+ };
31056
+ const noImpureStateUpdater = defineRule({
31057
+ id: "no-impure-state-updater",
31058
+ title: "State updater has side effects",
31059
+ severity: "error",
31060
+ recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
31061
+ create: (context) => ({ CallExpression(node) {
31062
+ const updater = node.arguments?.[0];
31063
+ if (!updater || !isFunctionLike$1(updater)) return;
31064
+ const analysis = getProgramAnalysis(node);
31065
+ if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
31066
+ const calleeReference = getRef(analysis, node.callee);
31067
+ if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
31068
+ const stateDeclarator = getUseStateDecl(analysis, calleeReference);
31069
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
31070
+ const operation = findImpureUpdaterOperation(updater, context.scopes);
31071
+ if (!operation) return;
31072
+ context.report({
31073
+ node: updater,
31074
+ message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
31075
+ });
31076
+ } })
31077
+ });
31078
+ //#endregion
30304
31079
  //#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
30305
31080
  const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
30306
31081
  const REACT_USE_REF_OPTIONS = {
@@ -30467,7 +31242,7 @@ const noInitializeState = defineRule({
30467
31242
  if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
30468
31243
  const analysis = getProgramAnalysis(node);
30469
31244
  if (!analysis) return;
30470
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
31245
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
30471
31246
  if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
30472
31247
  const stateName = getStateName(fact.stateDeclarator);
30473
31248
  context.report({
@@ -33469,6 +34244,166 @@ const isDirectParentCallbackRef = (analysis, ref) => {
33469
34244
  return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
33470
34245
  }));
33471
34246
  };
34247
+ const getDeclarationKind = (declarator) => {
34248
+ const declaration = declarator.parent;
34249
+ return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
34250
+ };
34251
+ const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
34252
+ const getDestructuredPropertyName = (bindingIdentifier) => {
34253
+ const property = bindingIdentifier.parent;
34254
+ if (!property || !isNodeOfType(property, "Property")) return null;
34255
+ if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
34256
+ return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
34257
+ };
34258
+ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
34259
+ const unwrappedExpression = stripParenExpression(expression);
34260
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
34261
+ const callbackReference = getRef(analysis, unwrappedExpression);
34262
+ const callbackVariable = callbackReference?.resolved;
34263
+ if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
34264
+ if (isProp(analysis, callbackReference)) {
34265
+ if (isWholePropsObjectReference(analysis, callbackReference)) return null;
34266
+ const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
34267
+ return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
34268
+ }
34269
+ if (hasMutableBindingWrite(callbackReference)) return null;
34270
+ const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
34271
+ if (definitions.length !== 1) return null;
34272
+ const declarator = definitions[0];
34273
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
34274
+ visitedVariables.add(callbackVariable);
34275
+ if (isNodeOfType(declarator.id, "ObjectPattern")) {
34276
+ const bindingIdentifier = callbackVariable.defs[0]?.name;
34277
+ const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
34278
+ const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
34279
+ return propertyName && propsReference ? propertyName : null;
34280
+ }
34281
+ if (!isNodeOfType(declarator.id, "Identifier")) return null;
34282
+ return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
34283
+ }
34284
+ if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
34285
+ const callbackName = getStaticMemberPropertyName(unwrappedExpression);
34286
+ if (!callbackName) return null;
34287
+ const receiver = stripParenExpression(unwrappedExpression.object);
34288
+ if (!isNodeOfType(receiver, "Identifier")) return null;
34289
+ const receiverReference = getRef(analysis, receiver);
34290
+ if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
34291
+ return callbackName;
34292
+ };
34293
+ const isNullishRefInitializer = (initializer) => {
34294
+ if (!initializer) return true;
34295
+ const unwrappedInitializer = stripParenExpression(initializer);
34296
+ if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
34297
+ if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
34298
+ return unwrappedInitializer.name === "undefined";
34299
+ };
34300
+ const getDirectComponentBodyStatement = (node, componentBody) => {
34301
+ let current = node;
34302
+ while (current?.parent && current.parent !== componentBody) current = current.parent;
34303
+ return current?.parent === componentBody ? current : null;
34304
+ };
34305
+ const getVariableForDeclarator = (analysis, declarator) => {
34306
+ for (const scope of analysis.scopeManager.scopes) {
34307
+ const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
34308
+ if (variable) return variable;
34309
+ }
34310
+ return null;
34311
+ };
34312
+ const getRefMember = (identifier) => {
34313
+ const receiver = findTransparentExpressionRoot(identifier);
34314
+ const member = receiver.parent;
34315
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
34316
+ return member;
34317
+ };
34318
+ const getRefMemberAssignment = (identifier) => {
34319
+ const member = getRefMember(identifier);
34320
+ if (!member) return null;
34321
+ const memberRoot = findTransparentExpressionRoot(member);
34322
+ const assignment = memberRoot.parent;
34323
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
34324
+ return assignment;
34325
+ };
34326
+ const getRefAliasDeclarator = (identifier) => {
34327
+ const initializer = findTransparentExpressionRoot(identifier);
34328
+ const declarator = initializer.parent;
34329
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
34330
+ return declarator;
34331
+ };
34332
+ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
34333
+ if (!isNodeOfType(receiver, "Identifier")) return null;
34334
+ const receiverReference = getRef(analysis, receiver);
34335
+ if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
34336
+ const variables = /* @__PURE__ */ new Set();
34337
+ let currentVariable = receiverReference.resolved;
34338
+ let refCall = null;
34339
+ while (currentVariable && !variables.has(currentVariable)) {
34340
+ variables.add(currentVariable);
34341
+ const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
34342
+ if (definitions.length !== 1) return null;
34343
+ const declarator = definitions[0];
34344
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
34345
+ if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
34346
+ refCall = declarator.init;
34347
+ break;
34348
+ }
34349
+ if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
34350
+ const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
34351
+ if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
34352
+ currentVariable = upstreamReference.resolved;
34353
+ }
34354
+ if (!refCall) return null;
34355
+ const pendingVariables = [...variables];
34356
+ while (pendingVariables.length > 0) {
34357
+ const variable = pendingVariables.pop();
34358
+ if (!variable) continue;
34359
+ for (const candidateReference of variable.references) {
34360
+ const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
34361
+ if (!aliasDeclarator) continue;
34362
+ const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
34363
+ if (!aliasVariable || variables.has(aliasVariable)) continue;
34364
+ if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
34365
+ variables.add(aliasVariable);
34366
+ pendingVariables.push(aliasVariable);
34367
+ }
34368
+ }
34369
+ return {
34370
+ refCall,
34371
+ variables
34372
+ };
34373
+ };
34374
+ const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
34375
+ const callee = stripParenExpression(callExpression.callee);
34376
+ if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
34377
+ const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
34378
+ if (!bindingProvenance) return null;
34379
+ const { refCall, variables } = bindingProvenance;
34380
+ const componentFunction = findEnclosingFunction(effectCall);
34381
+ if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
34382
+ if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
34383
+ const callbackPropNames = /* @__PURE__ */ new Set();
34384
+ const initializer = refCall.arguments?.[0];
34385
+ const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
34386
+ if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
34387
+ else if (!isNullishRefInitializer(initializer)) return null;
34388
+ for (const variable of variables) for (const candidateReference of variable.references) {
34389
+ if (candidateReference.init) continue;
34390
+ if (candidateReference.isWrite()) return null;
34391
+ const identifier = candidateReference.identifier;
34392
+ if (getRefAliasDeclarator(identifier)) continue;
34393
+ const member = getRefMember(identifier);
34394
+ if (!member || getStaticMemberPropertyName(member) !== "current") return null;
34395
+ const memberParent = findTransparentExpressionRoot(member).parent;
34396
+ if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
34397
+ const assignment = getRefMemberAssignment(identifier);
34398
+ if (!assignment) continue;
34399
+ const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
34400
+ if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
34401
+ const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
34402
+ if (!assignedCallbackName) return null;
34403
+ callbackPropNames.add(assignedCallbackName);
34404
+ }
34405
+ return callbackPropNames.size > 0 ? { callbackPropNames } : null;
34406
+ };
33472
34407
  const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
33473
34408
  const node = def.node;
33474
34409
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
@@ -33524,71 +34459,79 @@ const noPassDataToParent = defineRule({
33524
34459
  severity: "warn",
33525
34460
  tags: ["test-noise"],
33526
34461
  recommendation: "Fetch the data in the parent and pass it down as a prop (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent",
33527
- create: (context) => ({ CallExpression(node) {
33528
- if (!isUseEffect(node)) return;
33529
- const analysis = getProgramAnalysis(node);
33530
- if (!analysis) return;
33531
- if (hasCleanup(analysis, node)) return;
33532
- const effectFnRefs = getEffectFnRefs(analysis, node);
33533
- if (!effectFnRefs) return;
33534
- const effectFn = getEffectFn(analysis, node);
33535
- if (!effectFn) return;
33536
- for (const ref of effectFnRefs) {
33537
- const callExpr = getCallExpr(ref);
33538
- if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
33539
- if (isRefCall(analysis, ref)) continue;
33540
- if (!isSynchronous(ref.identifier, effectFn)) continue;
33541
- const calleeNode = unwrapChainExpression$1(callExpr.callee);
33542
- const identifier = ref.identifier;
33543
- if (calleeNode === identifier) {
33544
- if (!isDirectParentCallbackRef(analysis, ref)) continue;
33545
- if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
33546
- } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
33547
- if (!isWholePropsObjectReference(analysis, ref)) continue;
33548
- if (isCustomHookParameter(ref)) continue;
33549
- } else continue;
33550
- const methodName = getCallMethodName(calleeNode);
33551
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
33552
- if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
33553
- if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
33554
- if (isNamespacedApiCallee(calleeNode)) continue;
33555
- const calleeName = isNodeOfType(identifier, "Identifier") ? identifier.name : methodName;
33556
- const isSetterNamedCallee = Boolean(calleeName && SETTER_NAMED_PROP_PATTERN.test(calleeName));
33557
- const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
33558
- const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
33559
- if (isFunctionLike$1(argument)) {
33560
- if (!isSetterNamedCallee) return [];
33561
- return getFunctionalUpdaterDataRefs(analysis, argument);
33562
- }
33563
- if (isHandlerBagArgument(analysis, argument)) return [];
33564
- if (isParentWiredHookResultArgument(analysis, argument)) return [];
33565
- if (isNodeOfType(argument, "Identifier")) {
33566
- const argumentRef = getRef(analysis, argument);
33567
- if (argumentRef && resolveToFunction(argumentRef)) return [];
33568
- }
33569
- return getDownstreamRefs(analysis, argument);
33570
- }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
33571
- if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
33572
- if (!argsUpstreamRefs.some((argRef) => {
33573
- if (isUseStateIdentifier(argRef.identifier)) return false;
33574
- if (isProp(analysis, argRef)) return false;
33575
- if (isUseRefIdentifier(argRef.identifier)) return false;
33576
- if (isRefCurrent(argRef)) return false;
33577
- if (isConstant(argRef)) return false;
33578
- if (isParentWiredHookResultRef(analysis, argRef)) return false;
33579
- if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
33580
- if (resolvesToFunctionBinding(argRef)) return false;
33581
- const argIdentifier = argRef.identifier;
33582
- if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
33583
- if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
33584
- return true;
33585
- })) continue;
33586
- context.report({
33587
- node: callExpr,
33588
- message: "Handing data back to a parent from a useEffect costs your users an extra render."
33589
- });
33590
- }
33591
- } })
34462
+ create: (context) => {
34463
+ const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
34464
+ allowGlobalReactNamespace: true,
34465
+ allowUnboundBareCalls: true
34466
+ });
34467
+ return { CallExpression(node) {
34468
+ if (!isUseEffect(node)) return;
34469
+ const analysis = getProgramAnalysis(node);
34470
+ if (!analysis) return;
34471
+ if (hasCleanup(analysis, node)) return;
34472
+ const effectFnRefs = getEffectFnRefs(analysis, node);
34473
+ if (!effectFnRefs) return;
34474
+ const effectFn = getEffectFn(analysis, node);
34475
+ if (!effectFn) return;
34476
+ for (const ref of effectFnRefs) {
34477
+ const callExpr = getCallExpr(ref);
34478
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
34479
+ const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
34480
+ if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
34481
+ if (!isSynchronous(ref.identifier, effectFn)) continue;
34482
+ const calleeNode = unwrapChainExpression$1(callExpr.callee);
34483
+ const identifier = ref.identifier;
34484
+ if (callbackRefProvenance) {
34485
+ if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
34486
+ } else if (calleeNode === identifier) {
34487
+ if (!isDirectParentCallbackRef(analysis, ref)) continue;
34488
+ if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
34489
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
34490
+ if (!isWholePropsObjectReference(analysis, ref)) continue;
34491
+ if (isCustomHookParameter(ref)) continue;
34492
+ } else continue;
34493
+ const methodName = getCallMethodName(calleeNode);
34494
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
34495
+ if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
34496
+ if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
34497
+ if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
34498
+ const isSetterNamedCallee = callbackRefProvenance ? [...callbackRefProvenance.callbackPropNames].every((callbackPropName) => SETTER_NAMED_PROP_PATTERN.test(callbackPropName)) : Boolean((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) && SETTER_NAMED_PROP_PATTERN.test((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) ?? ""));
34499
+ const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
34500
+ const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
34501
+ if (isFunctionLike$1(argument)) {
34502
+ if (!isSetterNamedCallee) return [];
34503
+ return getFunctionalUpdaterDataRefs(analysis, argument);
34504
+ }
34505
+ if (isHandlerBagArgument(analysis, argument)) return [];
34506
+ if (isParentWiredHookResultArgument(analysis, argument)) return [];
34507
+ if (isNodeOfType(argument, "Identifier")) {
34508
+ const argumentRef = getRef(analysis, argument);
34509
+ if (argumentRef && resolveToFunction(argumentRef)) return [];
34510
+ }
34511
+ return getDownstreamRefs(analysis, argument);
34512
+ }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
34513
+ if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
34514
+ if (!argsUpstreamRefs.some((argRef) => {
34515
+ if (isUseStateIdentifier(argRef.identifier)) return false;
34516
+ if (isProp(analysis, argRef)) return false;
34517
+ if (isUseRefIdentifier(argRef.identifier)) return false;
34518
+ if (isRefCurrent(argRef)) return false;
34519
+ if (isConstant(argRef)) return false;
34520
+ if (isParentWiredHookResultRef(analysis, argRef)) return false;
34521
+ if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
34522
+ if (resolvesToFunctionBinding(argRef)) return false;
34523
+ const argIdentifier = argRef.identifier;
34524
+ if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
34525
+ if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
34526
+ return true;
34527
+ })) continue;
34528
+ context.report({
34529
+ node: callExpr,
34530
+ message: "Handing data back to a parent from a useEffect costs your users an extra render."
34531
+ });
34532
+ }
34533
+ } };
34534
+ }
33592
34535
  });
33593
34536
  //#endregion
33594
34537
  //#region src/plugin/utils/is-call-result-consumed-as-argument.ts
@@ -34142,6 +35085,66 @@ const noPropCallbackInEffect = defineRule({
34142
35085
  }
34143
35086
  });
34144
35087
  //#endregion
35088
+ //#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
35089
+ const isPreservedThroughConciseArrow = (callExpression, scopes) => {
35090
+ let node = callExpression;
35091
+ let parent = node.parent;
35092
+ while (parent) {
35093
+ if (isNodeOfType(parent, "ChainExpression")) {
35094
+ node = parent;
35095
+ parent = node.parent;
35096
+ continue;
35097
+ }
35098
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
35099
+ node = parent;
35100
+ parent = node.parent;
35101
+ continue;
35102
+ }
35103
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
35104
+ node = parent;
35105
+ parent = node.parent;
35106
+ continue;
35107
+ }
35108
+ if (isNodeOfType(parent, "SequenceExpression")) {
35109
+ const expressions = parent.expressions ?? [];
35110
+ if (expressions[expressions.length - 1] !== node) return false;
35111
+ node = parent;
35112
+ parent = node.parent;
35113
+ continue;
35114
+ }
35115
+ if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
35116
+ const invocation = parent.parent;
35117
+ if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
35118
+ if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
35119
+ const callee = stripParenExpression(invocation.callee);
35120
+ return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
35121
+ }
35122
+ node = invocation;
35123
+ parent = node.parent;
35124
+ }
35125
+ return false;
35126
+ };
35127
+ const noPropCallbackInRender = defineRule({
35128
+ id: "no-prop-callback-in-render",
35129
+ title: "Prop callback invoked during render",
35130
+ severity: "error",
35131
+ recommendation: "Invoke the callback from the event or asynchronous operation that produced the value, or from an effect when synchronizing with an external system. Render must stay pure because React can replay or discard it.",
35132
+ create: (context) => ({ CallExpression(node) {
35133
+ if (!isResultDiscardedCall(node)) return;
35134
+ if (isPreservedThroughConciseArrow(node, context.scopes)) return;
35135
+ if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
35136
+ const analysis = getProgramAnalysis(node);
35137
+ if (!analysis) return;
35138
+ const callee = stripParenExpression(node.callee);
35139
+ if (isFunctionLike$1(callee)) return;
35140
+ if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
35141
+ context.report({
35142
+ node,
35143
+ message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
35144
+ });
35145
+ } })
35146
+ });
35147
+ //#endregion
34145
35148
  //#region src/plugin/rules/architecture/no-prop-types.ts
34146
35149
  const PROP_TYPES_PROPERTY = "propTypes";
34147
35150
  const isPropTypesKey = (key, computed) => {
@@ -34836,6 +35839,63 @@ const noRedundantShouldComponentUpdate = defineRule({
34836
35839
  }
34837
35840
  });
34838
35841
  //#endregion
35842
+ //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
35843
+ const resolveReactRefSymbol = (memberExpression, scopes) => {
35844
+ const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
35845
+ if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
35846
+ const symbol = resolveConstIdentifierAlias(receiver, scopes);
35847
+ if (!symbol?.initializer) return null;
35848
+ const initializer = stripParenExpression(symbol.initializer);
35849
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
35850
+ return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
35851
+ };
35852
+ const isSameRefCurrentMember = (node, refSymbol, scopes) => {
35853
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
35854
+ const receiver = stripParenExpression(node.object);
35855
+ return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
35856
+ };
35857
+ const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
35858
+ if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
35859
+ let descendant = assignmentExpression;
35860
+ let ancestor = descendant.parent;
35861
+ while (ancestor) {
35862
+ if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
35863
+ const { left, right } = ancestor.test;
35864
+ if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
35865
+ }
35866
+ descendant = ancestor;
35867
+ ancestor = descendant.parent;
35868
+ }
35869
+ return false;
35870
+ };
35871
+ const noRefCurrentInRender = defineRule({
35872
+ id: "no-ref-current-in-render",
35873
+ title: "Ref mutated during render",
35874
+ severity: "error",
35875
+ recommendation: "Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.",
35876
+ create: (context) => {
35877
+ const report = (memberExpression) => {
35878
+ if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
35879
+ if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
35880
+ context.report({
35881
+ node: memberExpression,
35882
+ message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
35883
+ });
35884
+ };
35885
+ return {
35886
+ AssignmentExpression(node) {
35887
+ const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
35888
+ if (!refSymbol) return;
35889
+ if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
35890
+ report(node.left);
35891
+ },
35892
+ UpdateExpression(node) {
35893
+ report(node.argument);
35894
+ }
35895
+ };
35896
+ }
35897
+ });
35898
+ //#endregion
34839
35899
  //#region src/plugin/rules/architecture/no-render-in-render.ts
34840
35900
  const isInsideComponentContext = (node) => {
34841
35901
  let cursor = node.parent;
@@ -55798,6 +56858,18 @@ const reactDoctorRules = [
55798
56858
  requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
55799
56859
  }
55800
56860
  },
56861
+ {
56862
+ key: "react-doctor/no-impure-state-updater",
56863
+ id: "no-impure-state-updater",
56864
+ source: "react-doctor",
56865
+ originallyExternal: false,
56866
+ rule: {
56867
+ ...noImpureStateUpdater,
56868
+ framework: "global",
56869
+ category: "Bugs",
56870
+ requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
56871
+ }
56872
+ },
55801
56873
  {
55802
56874
  key: "react-doctor/no-indeterminate-attribute",
55803
56875
  id: "no-indeterminate-attribute",
@@ -56214,6 +57286,18 @@ const reactDoctorRules = [
56214
57286
  requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
56215
57287
  }
56216
57288
  },
57289
+ {
57290
+ key: "react-doctor/no-prop-callback-in-render",
57291
+ id: "no-prop-callback-in-render",
57292
+ source: "react-doctor",
57293
+ originallyExternal: false,
57294
+ rule: {
57295
+ ...noPropCallbackInRender,
57296
+ framework: "global",
57297
+ category: "Bugs",
57298
+ requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
57299
+ }
57300
+ },
56217
57301
  {
56218
57302
  key: "react-doctor/no-prop-types",
56219
57303
  id: "no-prop-types",
@@ -56305,6 +57389,18 @@ const reactDoctorRules = [
56305
57389
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
56306
57390
  }
56307
57391
  },
57392
+ {
57393
+ key: "react-doctor/no-ref-current-in-render",
57394
+ id: "no-ref-current-in-render",
57395
+ source: "react-doctor",
57396
+ originallyExternal: false,
57397
+ rule: {
57398
+ ...noRefCurrentInRender,
57399
+ framework: "global",
57400
+ category: "Bugs",
57401
+ requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
57402
+ }
57403
+ },
56308
57404
  {
56309
57405
  key: "react-doctor/no-render-in-render",
56310
57406
  id: "no-render-in-render",
@@ -58897,6 +59993,11 @@ const CROSS_FILE_RULE_IDS = new Set([
58897
59993
  "no-indeterminate-attribute",
58898
59994
  "no-locale-format-in-render",
58899
59995
  "no-match-media-in-state-initializer",
59996
+ "no-adjust-state-on-prop-change",
59997
+ "no-derived-state",
59998
+ "no-derived-state-effect",
59999
+ "no-event-handler",
60000
+ "no-initialize-state",
58900
60001
  "no-mutating-reducer-state",
58901
60002
  "no-unguarded-browser-global-in-render-or-hook-init",
58902
60003
  "prefer-dynamic-import",
@@ -58950,6 +60051,9 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
58950
60051
  if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
58951
60052
  for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
58952
60053
  };
60054
+ const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
60055
+ for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
60056
+ };
58953
60057
  const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
58954
60058
  const namedUseReducerLocals = /* @__PURE__ */ new Set();
58955
60059
  const reactObjectLocals = /* @__PURE__ */ new Set();
@@ -59036,6 +60140,11 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
59036
60140
  ["no-indeterminate-attribute", collectNearestManifestDependencies],
59037
60141
  ["no-locale-format-in-render", collectNearestManifestDependencies],
59038
60142
  ["no-match-media-in-state-initializer", collectNearestManifestDependencies],
60143
+ ["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
60144
+ ["no-derived-state", collectEffectValueHelperDependencies],
60145
+ ["no-derived-state-effect", collectEffectValueHelperDependencies],
60146
+ ["no-event-handler", collectEffectValueHelperDependencies],
60147
+ ["no-initialize-state", collectEffectValueHelperDependencies],
59039
60148
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
59040
60149
  ["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
59041
60150
  ["prefer-dynamic-import", collectNearestManifestDependencies],