oxlint-plugin-react-doctor 0.7.4-dev.2b5a18e → 0.7.4-dev.4a5d9bb

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 +0 -138
  2. package/dist/index.js +154 -948
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9968,18 +9968,13 @@ const unwrapExpression$3 = (node) => {
9968
9968
  return current;
9969
9969
  };
9970
9970
  /**
9971
- * Get the hook name from a direct, wrapped, namespaced, or immutable
9972
- * React import alias call.
9971
+ * Get the hook name from a call expression's callee, regardless of
9972
+ * whether the hook is called as `useFoo()` (Identifier) or
9973
+ * `React.useFoo()` (MemberExpression).
9973
9974
  */
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;
9975
+ const getHookName = (callee) => {
9976
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
9977
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9983
9978
  return null;
9984
9979
  };
9985
9980
  const FUNCTION_SCOPE_KINDS = new Set([
@@ -10164,7 +10159,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
10164
10159
  * - primitive-literal local consts (the value never changes
10165
10160
  * between renders unless the literal does)
10166
10161
  */
10167
- const symbolHasStableHookOrigin = (symbol, scopes) => {
10162
+ const symbolHasStableHookOrigin = (symbol) => {
10168
10163
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
10169
10164
  let declarator = symbol.declarationNode;
10170
10165
  while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
@@ -10177,7 +10172,7 @@ const symbolHasStableHookOrigin = (symbol, scopes) => {
10177
10172
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
10178
10173
  }
10179
10174
  if (!isNodeOfType(initializer, "CallExpression")) return false;
10180
- const initializerHookName = getHookName(initializer.callee, scopes);
10175
+ const initializerHookName = getHookName(initializer.callee);
10181
10176
  if (!initializerHookName) return false;
10182
10177
  if (initializerHookName === "useRef") return true;
10183
10178
  if (initializerHookName === "useEffectEvent") return true;
@@ -10212,7 +10207,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
10212
10207
  if (!declarator.init) return false;
10213
10208
  const initializer = unwrapExpression$3(declarator.init);
10214
10209
  if (!isNodeOfType(initializer, "CallExpression")) return false;
10215
- const initializerHookName = getHookName(initializer.callee, scopes);
10210
+ const initializerHookName = getHookName(initializer.callee);
10216
10211
  if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
10217
10212
  const depsArgument = initializer.arguments[1];
10218
10213
  if (!depsArgument || !isAstNode(depsArgument)) return false;
@@ -10242,7 +10237,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
10242
10237
  }
10243
10238
  return null;
10244
10239
  };
10245
- const isStableRefContainerCapture = (symbol, depKey, scopes) => {
10240
+ const isStableRefContainerCapture = (symbol, depKey) => {
10246
10241
  if (symbol.kind !== "const") return false;
10247
10242
  if (!depKey.startsWith(`${symbol.name}.`)) return false;
10248
10243
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
@@ -10256,7 +10251,7 @@ const isStableRefContainerCapture = (symbol, depKey, scopes) => {
10256
10251
  if (!propertyValue) return false;
10257
10252
  currentValue = propertyValue;
10258
10253
  }
10259
- return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
10254
+ return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
10260
10255
  };
10261
10256
  const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
10262
10257
  if (visitedSymbolIds.has(symbol.id)) return true;
@@ -10274,13 +10269,7 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
10274
10269
  }
10275
10270
  return true;
10276
10271
  };
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);
10272
+ const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
10284
10273
  //#endregion
10285
10274
  //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
10286
10275
  const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
@@ -10468,18 +10457,10 @@ const collectCaptureDepKeys = (callback, scopes) => {
10468
10457
  }
10469
10458
  const depKey = computeDepKey(reference);
10470
10459
  if (!depKey) continue;
10471
- if (isStableRefContainerCapture(symbol, depKey, scopes)) {
10460
+ if (isStableRefContainerCapture(symbol, depKey)) {
10472
10461
  stableCapturedNames.add(depKey);
10473
10462
  continue;
10474
10463
  }
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
- }
10483
10464
  keys.add(depKey);
10484
10465
  }
10485
10466
  return {
@@ -10508,60 +10489,12 @@ const hasComputedMemberExpression = (node) => {
10508
10489
  if (stripped.computed) return true;
10509
10490
  return hasComputedMemberExpression(stripped.object);
10510
10491
  };
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
- };
10559
10492
  const isUseCallbackResultDep = (node, scopes) => {
10560
10493
  const rootSymbol = getRootSymbol(node, scopes);
10561
10494
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
10562
- return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
10495
+ return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
10563
10496
  };
10564
- const isExtraReactiveDepAllowed = (node, scopes) => {
10497
+ const isExtraEffectDepAllowed = (node, scopes) => {
10565
10498
  const rootIdentifier = getMemberRootIdentifier(node);
10566
10499
  if (!rootIdentifier) return false;
10567
10500
  const symbol = scopes.symbolFor(rootIdentifier);
@@ -10589,78 +10522,12 @@ const isUnstableInitializer = (node) => {
10589
10522
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
10590
10523
  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");
10591
10524
  };
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
- };
10612
10525
  const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
10613
10526
  const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
10614
- const isStableSetterLikeSymbol = (symbol, scopes) => {
10615
- if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
10527
+ const isStableSetterLikeSymbol = (symbol) => {
10528
+ if (!symbolHasStableHookOrigin(symbol)) return false;
10616
10529
  return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
10617
10530
  };
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
- };
10664
10531
  const findStableSetterReference = (node, scopes) => {
10665
10532
  let setterName = null;
10666
10533
  const visit = (current) => {
@@ -10668,7 +10535,7 @@ const findStableSetterReference = (node, scopes) => {
10668
10535
  if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
10669
10536
  if (isNodeOfType(current, "Identifier")) {
10670
10537
  const symbol = scopes.referenceFor(current)?.resolvedSymbol;
10671
- if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
10538
+ if (symbol && isStableSetterLikeSymbol(symbol)) {
10672
10539
  setterName = symbol.name;
10673
10540
  return;
10674
10541
  }
@@ -10784,11 +10651,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
10784
10651
  }
10785
10652
  return false;
10786
10653
  };
10787
- const isSeededDataRefSymbol = (refSymbol, scopes) => {
10654
+ const isSeededDataRefSymbol = (refSymbol) => {
10788
10655
  if (!refSymbol) return false;
10789
10656
  const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
10790
10657
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
10791
- if (getHookName(initializer.callee, scopes) !== "useRef") return false;
10658
+ if (getHookName(initializer.callee) !== "useRef") return false;
10792
10659
  const firstArgument = initializer.arguments[0];
10793
10660
  if (!firstArgument || !isAstNode(firstArgument)) return false;
10794
10661
  const strippedArgument = unwrapExpression$3(firstArgument);
@@ -10828,9 +10695,7 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
10828
10695
  const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
10829
10696
  return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
10830
10697
  };
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;
10698
+ const hasMemberCallForRoot = (node, rootName) => {
10834
10699
  let didFindMemberCall = false;
10835
10700
  const visit = (current) => {
10836
10701
  if (didFindMemberCall) return;
@@ -10846,7 +10711,7 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
10846
10711
  }
10847
10712
  chainObject = unwrapExpression$3(chainObject.object);
10848
10713
  }
10849
- if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
10714
+ if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
10850
10715
  didFindMemberCall = true;
10851
10716
  return;
10852
10717
  }
@@ -10864,11 +10729,9 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
10864
10729
  visit(node);
10865
10730
  return didFindMemberCall;
10866
10731
  };
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");
10732
+ const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
10733
+ if ([...captureKeys].filter((captureKey) => captureKey.startsWith("props.")).length < 2 || declaredKeys.has("props")) return;
10734
+ if (hasMemberCallForRoot(callback, "props")) captureKeys.add("props");
10872
10735
  };
10873
10736
  const exhaustiveDeps = defineRule({
10874
10737
  id: "exhaustive-deps",
@@ -10923,7 +10786,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10923
10786
  };
10924
10787
  const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
10925
10788
  return { CallExpression(node) {
10926
- const hookName = getHookName(node.callee, context.scopes);
10789
+ const hookName = getHookName(node.callee);
10927
10790
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
10928
10791
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10929
10792
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
@@ -10980,7 +10843,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10980
10843
  if (outerAssignments.length > 0) return;
10981
10844
  const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
10982
10845
  const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
10983
- if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
10846
+ if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
10984
10847
  node: callbackToAnalyze,
10985
10848
  message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
10986
10849
  });
@@ -11079,7 +10942,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
11079
10942
  const fullChain = stringifyMemberChain(stripped);
11080
10943
  if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
11081
10944
  const refSymbol = context.scopes.symbolFor(stripped.object);
11082
- if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
10945
+ if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
11083
10946
  if (!didReportRefCurrentDep) {
11084
10947
  context.report({
11085
10948
  node: elementNode,
@@ -11117,7 +10980,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
11117
10980
  declaredKeys.add(key);
11118
10981
  declaredKeyToReportNode.set(key, elementNode);
11119
10982
  }
11120
- addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
10983
+ addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
11121
10984
  const missingCaptureKeys = [];
11122
10985
  for (const captureKey of captureKeys) {
11123
10986
  let isCoveredByDeclared = false;
@@ -11203,7 +11066,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
11203
11066
  if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
11204
11067
  if (outerFunctionCapturedNames.has(rootName)) continue;
11205
11068
  const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
11206
- if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
11069
+ if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) && isExtraEffectDepAllowed(reportNode, context.scopes)) continue;
11207
11070
  if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
11208
11071
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
11209
11072
  if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
@@ -22429,26 +22292,6 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
22429
22292
  const importDeclaration = declarationNode.parent;
22430
22293
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22431
22294
  }));
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
- };
22452
22295
  const isHookCallee$1 = (analysis, node, hookName) => {
22453
22296
  if (!node) return false;
22454
22297
  if (isNodeOfType(node, "Identifier")) {
@@ -23503,20 +23346,6 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23503
23346
  }
23504
23347
  return evidence;
23505
23348
  };
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
- };
23520
23349
  const findStateSetterReference = (analysis, callExpression) => {
23521
23350
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
23522
23351
  const callee = stripParenExpression(callExpression.callee);
@@ -26569,6 +26398,57 @@ const noDefaultProps = defineRule({
26569
26398
  } })
26570
26399
  });
26571
26400
  //#endregion
26401
+ //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26402
+ const getStateName$1 = (stateDeclarator) => {
26403
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26404
+ if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26405
+ const stateBinding = stateDeclarator.id.elements?.[0];
26406
+ if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26407
+ const setterBinding = stateDeclarator.id.elements?.[1];
26408
+ if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26409
+ if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26410
+ return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26411
+ };
26412
+ const noDerivedState = defineRule({
26413
+ id: "no-derived-state",
26414
+ title: "Derived value copied into state",
26415
+ severity: "warn",
26416
+ tags: ["test-noise"],
26417
+ 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",
26418
+ create: (context) => ({ CallExpression(node) {
26419
+ if (!isUseEffect(node)) return;
26420
+ const analysis = getProgramAnalysis(node);
26421
+ if (!analysis) return;
26422
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
26423
+ if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26424
+ const stateName = getStateName$1(fact.stateDeclarator);
26425
+ context.report({
26426
+ node: fact.callExpression,
26427
+ message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26428
+ });
26429
+ }
26430
+ } })
26431
+ });
26432
+ //#endregion
26433
+ //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26434
+ const noDerivedStateEffect = defineRule({
26435
+ id: "no-derived-state-effect",
26436
+ title: "Derived state stored in an effect",
26437
+ severity: "warn",
26438
+ tags: ["test-noise"],
26439
+ 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",
26440
+ create: (context) => ({ CallExpression(node) {
26441
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26442
+ const analysis = getProgramAnalysis(node);
26443
+ if (!analysis) return;
26444
+ if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26445
+ context.report({
26446
+ node,
26447
+ message: "You pay an extra render for state you can derive from other values."
26448
+ });
26449
+ } })
26450
+ });
26451
+ //#endregion
26572
26452
  //#region src/plugin/utils/is-component-function.ts
26573
26453
  const isFunctionAssignedToComponent = (functionNode) => {
26574
26454
  let cursor = functionNode.parent ?? null;
@@ -26702,236 +26582,6 @@ const createComponentPropStackTracker = (callbacks) => {
26702
26582
  };
26703
26583
  };
26704
26584
  //#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
26935
26585
  //#region src/plugin/utils/is-initial-only-prop-name.ts
26936
26586
  const isInitialOnlyPropName = (propName) => {
26937
26587
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -30295,26 +29945,28 @@ const noGiantComponent = defineRule({
30295
29945
  const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
30296
29946
  return lineCount > 300 ? lineCount : null;
30297
29947
  };
30298
- const reportOversizedComponent = (nameNode, componentName) => {
29948
+ const reportOversizedComponent = (nameNode, componentName, lineCount) => {
30299
29949
  context.report({
30300
29950
  node: nameNode,
30301
- message: `Component "${componentName}" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.`
29951
+ message: `Component "${componentName}" is ${lineCount} lines long, which is hard to read & change. Split it into a few smaller components.`
30302
29952
  });
30303
29953
  };
30304
29954
  return {
30305
29955
  FunctionDeclaration(node) {
30306
29956
  if (!node.id?.name || !isUppercaseName(node.id.name)) return;
30307
- if (getOversizedComponentLineCount(node) === null) return;
29957
+ const lineCount = getOversizedComponentLineCount(node);
29958
+ if (lineCount === null) return;
30308
29959
  if (!functionContainsReactRenderOutput(node, context.scopes)) return;
30309
- reportOversizedComponent(node.id, node.id.name);
29960
+ reportOversizedComponent(node.id, node.id.name, lineCount);
30310
29961
  },
30311
29962
  VariableDeclarator(node) {
30312
29963
  if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
30313
29964
  const functionNode = unwrapReactHocFunction(node.init);
30314
29965
  if (!functionNode) return;
30315
- if (getOversizedComponentLineCount(functionNode) === null) return;
29966
+ const lineCount = getOversizedComponentLineCount(functionNode);
29967
+ if (lineCount === null) return;
30316
29968
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
30317
- reportOversizedComponent(node.id, node.id.name);
29969
+ reportOversizedComponent(node.id, node.id.name, lineCount);
30318
29970
  }
30319
29971
  };
30320
29972
  }
@@ -30951,131 +30603,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
30951
30603
  } })
30952
30604
  });
30953
30605
  //#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
31079
30606
  //#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
31080
30607
  const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
31081
30608
  const REACT_USE_REF_OPTIONS = {
@@ -34244,166 +33771,6 @@ const isDirectParentCallbackRef = (analysis, ref) => {
34244
33771
  return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
34245
33772
  }));
34246
33773
  };
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
- };
34407
33774
  const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
34408
33775
  const node = def.node;
34409
33776
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
@@ -34459,79 +33826,71 @@ const noPassDataToParent = defineRule({
34459
33826
  severity: "warn",
34460
33827
  tags: ["test-noise"],
34461
33828
  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",
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
- }
33829
+ create: (context) => ({ CallExpression(node) {
33830
+ if (!isUseEffect(node)) return;
33831
+ const analysis = getProgramAnalysis(node);
33832
+ if (!analysis) return;
33833
+ if (hasCleanup(analysis, node)) return;
33834
+ const effectFnRefs = getEffectFnRefs(analysis, node);
33835
+ if (!effectFnRefs) return;
33836
+ const effectFn = getEffectFn(analysis, node);
33837
+ if (!effectFn) return;
33838
+ for (const ref of effectFnRefs) {
33839
+ const callExpr = getCallExpr(ref);
33840
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
33841
+ if (isRefCall(analysis, ref)) continue;
33842
+ if (!isSynchronous(ref.identifier, effectFn)) continue;
33843
+ const calleeNode = unwrapChainExpression$1(callExpr.callee);
33844
+ const identifier = ref.identifier;
33845
+ if (calleeNode === identifier) {
33846
+ if (!isDirectParentCallbackRef(analysis, ref)) continue;
33847
+ if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
33848
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
33849
+ if (!isWholePropsObjectReference(analysis, ref)) continue;
33850
+ if (isCustomHookParameter(ref)) continue;
33851
+ } else continue;
33852
+ const methodName = getCallMethodName(calleeNode);
33853
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
33854
+ if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
33855
+ if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
33856
+ if (isNamespacedApiCallee(calleeNode)) continue;
33857
+ const calleeName = isNodeOfType(identifier, "Identifier") ? identifier.name : methodName;
33858
+ const isSetterNamedCallee = Boolean(calleeName && SETTER_NAMED_PROP_PATTERN.test(calleeName));
33859
+ const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
33860
+ const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
33861
+ if (isFunctionLike$1(argument)) {
33862
+ if (!isSetterNamedCallee) return [];
33863
+ return getFunctionalUpdaterDataRefs(analysis, argument);
33864
+ }
33865
+ if (isHandlerBagArgument(analysis, argument)) return [];
33866
+ if (isParentWiredHookResultArgument(analysis, argument)) return [];
33867
+ if (isNodeOfType(argument, "Identifier")) {
33868
+ const argumentRef = getRef(analysis, argument);
33869
+ if (argumentRef && resolveToFunction(argumentRef)) return [];
33870
+ }
33871
+ return getDownstreamRefs(analysis, argument);
33872
+ }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
33873
+ if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
33874
+ if (!argsUpstreamRefs.some((argRef) => {
33875
+ if (isUseStateIdentifier(argRef.identifier)) return false;
33876
+ if (isProp(analysis, argRef)) return false;
33877
+ if (isUseRefIdentifier(argRef.identifier)) return false;
33878
+ if (isRefCurrent(argRef)) return false;
33879
+ if (isConstant(argRef)) return false;
33880
+ if (isParentWiredHookResultRef(analysis, argRef)) return false;
33881
+ if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
33882
+ if (resolvesToFunctionBinding(argRef)) return false;
33883
+ const argIdentifier = argRef.identifier;
33884
+ if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
33885
+ if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
33886
+ return true;
33887
+ })) continue;
33888
+ context.report({
33889
+ node: callExpr,
33890
+ message: "Handing data back to a parent from a useEffect costs your users an extra render."
33891
+ });
33892
+ }
33893
+ } })
34535
33894
  });
34536
33895
  //#endregion
34537
33896
  //#region src/plugin/utils/is-call-result-consumed-as-argument.ts
@@ -35085,66 +34444,6 @@ const noPropCallbackInEffect = defineRule({
35085
34444
  }
35086
34445
  });
35087
34446
  //#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
35148
34447
  //#region src/plugin/rules/architecture/no-prop-types.ts
35149
34448
  const PROP_TYPES_PROPERTY = "propTypes";
35150
34449
  const isPropTypesKey = (key, computed) => {
@@ -35839,63 +35138,6 @@ const noRedundantShouldComponentUpdate = defineRule({
35839
35138
  }
35840
35139
  });
35841
35140
  //#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
35899
35141
  //#region src/plugin/rules/architecture/no-render-in-render.ts
35900
35142
  const isInsideComponentContext = (node) => {
35901
35143
  let cursor = node.parent;
@@ -56858,18 +56100,6 @@ const reactDoctorRules = [
56858
56100
  requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
56859
56101
  }
56860
56102
  },
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
- },
56873
56103
  {
56874
56104
  key: "react-doctor/no-indeterminate-attribute",
56875
56105
  id: "no-indeterminate-attribute",
@@ -57286,18 +56516,6 @@ const reactDoctorRules = [
57286
56516
  requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
57287
56517
  }
57288
56518
  },
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
- },
57301
56519
  {
57302
56520
  key: "react-doctor/no-prop-types",
57303
56521
  id: "no-prop-types",
@@ -57389,18 +56607,6 @@ const reactDoctorRules = [
57389
56607
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
57390
56608
  }
57391
56609
  },
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
- },
57404
56610
  {
57405
56611
  key: "react-doctor/no-render-in-render",
57406
56612
  id: "no-render-in-render",