oxlint-plugin-react-doctor 0.7.4-dev.11333b9 → 0.7.4-dev.118f806

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 +361 -1411
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9191,24 +9191,6 @@ 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
- };
9212
9194
  const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
9213
9195
  let currentNode = resourceNode;
9214
9196
  let parentNode = currentNode.parent;
@@ -9230,8 +9212,6 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
9230
9212
  if (!body) return null;
9231
9213
  let leak = null;
9232
9214
  const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
9233
- const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
9234
- const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
9235
9215
  walkAst(body, (child) => {
9236
9216
  if (leak !== null) return false;
9237
9217
  if (isFunctionLike$1(child)) return false;
@@ -9246,7 +9226,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
9246
9226
  eventKey: null,
9247
9227
  handlerKey: null
9248
9228
  };
9249
- if (!hasReleaseForUsage(socketUsage)) {
9229
+ if (!fileContainsReleaseForUsage(socketUsage, context)) {
9250
9230
  leak = socketUsage;
9251
9231
  return false;
9252
9232
  }
@@ -9263,7 +9243,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
9263
9243
  eventKey: null,
9264
9244
  handlerKey: null
9265
9245
  };
9266
- if (!hasReleaseForUsage(timerUsage)) {
9246
+ if (!fileContainsReleaseForUsage(timerUsage, context)) {
9267
9247
  leak = timerUsage;
9268
9248
  return false;
9269
9249
  }
@@ -9277,7 +9257,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
9277
9257
  handleKey: findAssignedResourceKey(child, context),
9278
9258
  ...registrationDetails
9279
9259
  };
9280
- if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
9260
+ if (!hasSelfReleasingListenerOptions(child, context) && !fileContainsReleaseForUsage(subscriptionUsage, context)) leak = subscriptionUsage;
9281
9261
  return false;
9282
9262
  }
9283
9263
  });
@@ -9968,18 +9948,13 @@ const unwrapExpression$3 = (node) => {
9968
9948
  return current;
9969
9949
  };
9970
9950
  /**
9971
- * Get the hook name from a direct, wrapped, namespaced, or immutable
9972
- * React import alias call.
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).
9973
9954
  */
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;
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;
9983
9958
  return null;
9984
9959
  };
9985
9960
  const FUNCTION_SCOPE_KINDS = new Set([
@@ -10164,7 +10139,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
10164
10139
  * - primitive-literal local consts (the value never changes
10165
10140
  * between renders unless the literal does)
10166
10141
  */
10167
- const symbolHasStableHookOrigin = (symbol, scopes) => {
10142
+ const symbolHasStableHookOrigin = (symbol) => {
10168
10143
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
10169
10144
  let declarator = symbol.declarationNode;
10170
10145
  while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
@@ -10177,7 +10152,7 @@ const symbolHasStableHookOrigin = (symbol, scopes) => {
10177
10152
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
10178
10153
  }
10179
10154
  if (!isNodeOfType(initializer, "CallExpression")) return false;
10180
- const initializerHookName = getHookName(initializer.callee, scopes);
10155
+ const initializerHookName = getHookName(initializer.callee);
10181
10156
  if (!initializerHookName) return false;
10182
10157
  if (initializerHookName === "useRef") return true;
10183
10158
  if (initializerHookName === "useEffectEvent") return true;
@@ -10212,7 +10187,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
10212
10187
  if (!declarator.init) return false;
10213
10188
  const initializer = unwrapExpression$3(declarator.init);
10214
10189
  if (!isNodeOfType(initializer, "CallExpression")) return false;
10215
- const initializerHookName = getHookName(initializer.callee, scopes);
10190
+ const initializerHookName = getHookName(initializer.callee);
10216
10191
  if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
10217
10192
  const depsArgument = initializer.arguments[1];
10218
10193
  if (!depsArgument || !isAstNode(depsArgument)) return false;
@@ -10242,7 +10217,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
10242
10217
  }
10243
10218
  return null;
10244
10219
  };
10245
- const isStableRefContainerCapture = (symbol, depKey, scopes) => {
10220
+ const isStableRefContainerCapture = (symbol, depKey) => {
10246
10221
  if (symbol.kind !== "const") return false;
10247
10222
  if (!depKey.startsWith(`${symbol.name}.`)) return false;
10248
10223
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
@@ -10256,7 +10231,7 @@ const isStableRefContainerCapture = (symbol, depKey, scopes) => {
10256
10231
  if (!propertyValue) return false;
10257
10232
  currentValue = propertyValue;
10258
10233
  }
10259
- return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
10234
+ return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
10260
10235
  };
10261
10236
  const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
10262
10237
  if (visitedSymbolIds.has(symbol.id)) return true;
@@ -10274,13 +10249,7 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
10274
10249
  }
10275
10250
  return true;
10276
10251
  };
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);
10252
+ const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
10284
10253
  //#endregion
10285
10254
  //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
10286
10255
  const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
@@ -10468,18 +10437,10 @@ const collectCaptureDepKeys = (callback, scopes) => {
10468
10437
  }
10469
10438
  const depKey = computeDepKey(reference);
10470
10439
  if (!depKey) continue;
10471
- if (isStableRefContainerCapture(symbol, depKey, scopes)) {
10440
+ if (isStableRefContainerCapture(symbol, depKey)) {
10472
10441
  stableCapturedNames.add(depKey);
10473
10442
  continue;
10474
10443
  }
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
10444
  keys.add(depKey);
10484
10445
  }
10485
10446
  return {
@@ -10508,60 +10469,12 @@ const hasComputedMemberExpression = (node) => {
10508
10469
  if (stripped.computed) return true;
10509
10470
  return hasComputedMemberExpression(stripped.object);
10510
10471
  };
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
10472
  const isUseCallbackResultDep = (node, scopes) => {
10560
10473
  const rootSymbol = getRootSymbol(node, scopes);
10561
10474
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
10562
- return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
10475
+ return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
10563
10476
  };
10564
- const isExtraReactiveDepAllowed = (node, scopes) => {
10477
+ const isExtraEffectDepAllowed = (node, scopes) => {
10565
10478
  const rootIdentifier = getMemberRootIdentifier(node);
10566
10479
  if (!rootIdentifier) return false;
10567
10480
  const symbol = scopes.symbolFor(rootIdentifier);
@@ -10589,17 +10502,10 @@ const isUnstableInitializer = (node) => {
10589
10502
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
10590
10503
  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
10504
  };
10592
- const isExtraDepAllowedForHook = (hookName, node, scopes) => {
10593
- if (!isExtraReactiveDepAllowed(node, scopes)) return false;
10594
- if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
10595
- if (hookName !== "useMemo") return false;
10596
- const rootSymbol = getRootSymbol(node, scopes);
10597
- return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
10598
- };
10599
10505
  const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
10600
10506
  const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
10601
- const isStableSetterLikeSymbol = (symbol, scopes) => {
10602
- if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
10507
+ const isStableSetterLikeSymbol = (symbol) => {
10508
+ if (!symbolHasStableHookOrigin(symbol)) return false;
10603
10509
  return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
10604
10510
  };
10605
10511
  const findStableSetterReference = (node, scopes) => {
@@ -10609,7 +10515,7 @@ const findStableSetterReference = (node, scopes) => {
10609
10515
  if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
10610
10516
  if (isNodeOfType(current, "Identifier")) {
10611
10517
  const symbol = scopes.referenceFor(current)?.resolvedSymbol;
10612
- if (symbol && isStableSetterLikeSymbol(symbol, scopes)) {
10518
+ if (symbol && isStableSetterLikeSymbol(symbol)) {
10613
10519
  setterName = symbol.name;
10614
10520
  return;
10615
10521
  }
@@ -10725,11 +10631,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
10725
10631
  }
10726
10632
  return false;
10727
10633
  };
10728
- const isSeededDataRefSymbol = (refSymbol, scopes) => {
10634
+ const isSeededDataRefSymbol = (refSymbol) => {
10729
10635
  if (!refSymbol) return false;
10730
10636
  const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
10731
10637
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
10732
- if (getHookName(initializer.callee, scopes) !== "useRef") return false;
10638
+ if (getHookName(initializer.callee) !== "useRef") return false;
10733
10639
  const firstArgument = initializer.arguments[0];
10734
10640
  if (!firstArgument || !isAstNode(firstArgument)) return false;
10735
10641
  const strippedArgument = unwrapExpression$3(firstArgument);
@@ -10769,9 +10675,7 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
10769
10675
  const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
10770
10676
  return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
10771
10677
  };
10772
- const hasMemberCallForRoot = (node, rootName, scopes) => {
10773
- const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
10774
- if (!rootSymbol) return false;
10678
+ const hasMemberCallForRoot = (node, rootName) => {
10775
10679
  let didFindMemberCall = false;
10776
10680
  const visit = (current) => {
10777
10681
  if (didFindMemberCall) return;
@@ -10787,7 +10691,7 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
10787
10691
  }
10788
10692
  chainObject = unwrapExpression$3(chainObject.object);
10789
10693
  }
10790
- if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
10694
+ if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
10791
10695
  didFindMemberCall = true;
10792
10696
  return;
10793
10697
  }
@@ -10805,11 +10709,9 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
10805
10709
  visit(node);
10806
10710
  return didFindMemberCall;
10807
10711
  };
10808
- const addAggregatePropsDependency = (captureKeys, declaredKeys, callback, scopes) => {
10809
- const propsCaptureKeys = [...captureKeys].filter((captureKey) => captureKey.startsWith("props."));
10810
- if (propsCaptureKeys.length < 2 || declaredKeys.has("props")) return;
10811
- if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
10812
- if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
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");
10813
10715
  };
10814
10716
  const exhaustiveDeps = defineRule({
10815
10717
  id: "exhaustive-deps",
@@ -10864,7 +10766,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10864
10766
  };
10865
10767
  const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
10866
10768
  return { CallExpression(node) {
10867
- const hookName = getHookName(node.callee, context.scopes);
10769
+ const hookName = getHookName(node.callee);
10868
10770
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
10869
10771
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10870
10772
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
@@ -10921,7 +10823,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10921
10823
  if (outerAssignments.length > 0) return;
10922
10824
  const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
10923
10825
  const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
10924
- if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
10826
+ if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
10925
10827
  node: callbackToAnalyze,
10926
10828
  message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
10927
10829
  });
@@ -11020,7 +10922,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
11020
10922
  const fullChain = stringifyMemberChain(stripped);
11021
10923
  if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
11022
10924
  const refSymbol = context.scopes.symbolFor(stripped.object);
11023
- if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
10925
+ if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
11024
10926
  if (!didReportRefCurrentDep) {
11025
10927
  context.report({
11026
10928
  node: elementNode,
@@ -11058,7 +10960,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
11058
10960
  declaredKeys.add(key);
11059
10961
  declaredKeyToReportNode.set(key, elementNode);
11060
10962
  }
11061
- addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
10963
+ addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
11062
10964
  const missingCaptureKeys = [];
11063
10965
  for (const captureKey of captureKeys) {
11064
10966
  let isCoveredByDeclared = false;
@@ -11144,7 +11046,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
11144
11046
  if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
11145
11047
  if (outerFunctionCapturedNames.has(rootName)) continue;
11146
11048
  const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
11147
- if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
11049
+ if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) && isExtraEffectDepAllowed(reportNode, context.scopes)) continue;
11148
11050
  if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
11149
11051
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
11150
11052
  if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
@@ -21086,7 +20988,6 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
21086
20988
  continue;
21087
20989
  }
21088
20990
  if (isNodeOfType(statement, "ExportNamedDeclaration")) {
21089
- if (statement.exportKind === "type") continue;
21090
20991
  const declaration = statement.declaration;
21091
20992
  if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
21092
20993
  recordVariableDeclaration(declaration);
@@ -21101,7 +21002,6 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
21101
21002
  }
21102
21003
  for (const specifier of statement.specifiers ?? []) {
21103
21004
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21104
- if (specifier.exportKind === "type") continue;
21105
21005
  const local = specifier.local;
21106
21006
  const exported = specifier.exported;
21107
21007
  if (!isNodeOfType(local, "Identifier")) continue;
@@ -21150,37 +21050,25 @@ const resolveImportedExportName = (importSpecifier) => {
21150
21050
  if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
21151
21051
  return null;
21152
21052
  };
21153
- const findReExportTargetsForName = (programRoot, exportedName) => {
21053
+ const findReExportSourcesForName = (programRoot, exportedName) => {
21154
21054
  if (!isNodeOfType(programRoot, "Program")) return [];
21155
- const exportAllTargets = [];
21055
+ const exportAllSources = [];
21156
21056
  for (const statement of programRoot.body ?? []) {
21157
21057
  if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
21158
- if (statement.exportKind === "type") continue;
21159
21058
  const sourceValue = statement.source.value;
21160
21059
  if (typeof sourceValue !== "string") continue;
21161
21060
  for (const specifier of statement.specifiers ?? []) {
21162
21061
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21163
- if (specifier.exportKind === "type") continue;
21164
21062
  const exported = specifier.exported;
21165
- if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
21166
- const local = specifier.local;
21167
- const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
21168
- if (importedName) return [{
21169
- importedName,
21170
- source: sourceValue
21171
- }];
21063
+ if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
21172
21064
  }
21173
21065
  }
21174
21066
  if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
21175
- if (statement.exportKind === "type" || statement.exported) continue;
21176
21067
  const sourceValue = statement.source.value;
21177
- if (typeof sourceValue === "string") exportAllTargets.push({
21178
- importedName: exportedName,
21179
- source: sourceValue
21180
- });
21068
+ if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
21181
21069
  }
21182
21070
  }
21183
- return exportAllTargets;
21071
+ return exportAllSources;
21184
21072
  };
21185
21073
  //#endregion
21186
21074
  //#region src/plugin/utils/attach-parent-references.ts
@@ -21269,6 +21157,139 @@ const parseSourceFile = (absoluteFilePath) => {
21269
21157
  return parsedProgram;
21270
21158
  };
21271
21159
  //#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
21272
21293
  //#region src/plugin/utils/resolve-relative-import-path.ts
21273
21294
  const MODULE_FILE_EXTENSIONS = [
21274
21295
  ".ts",
@@ -21385,6 +21406,35 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
21385
21406
  };
21386
21407
  const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
21387
21408
  //#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
21388
21438
  //#region src/plugin/utils/resolve-tsconfig-alias.ts
21389
21439
  const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
21390
21440
  const isObjectRecord = (value) => typeof value === "object" && value !== null;
@@ -21563,29 +21613,25 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
21563
21613
  };
21564
21614
  //#endregion
21565
21615
  //#region src/plugin/utils/resolve-module-path.ts
21566
- const resolveModulePath = (fromFilename, source) => {
21567
- if (path.isAbsolute(source)) return null;
21568
- return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
21569
- };
21616
+ const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
21570
21617
  //#endregion
21571
21618
  //#region src/plugin/utils/resolve-cross-file-function-export.ts
21572
21619
  const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
21573
21620
  if (visitedFilePaths.size >= 4) return null;
21574
21621
  if (visitedFilePaths.has(filePath)) return null;
21575
21622
  visitedFilePaths.add(filePath);
21576
- const programRoot = parseSourceFile(filePath);
21623
+ const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
21624
+ const programRoot = parseSourceFile(actualFilePath);
21577
21625
  if (!programRoot) return null;
21578
21626
  const exported = findExportedFunctionBody(programRoot, exportedName);
21579
21627
  if (exported) return exported;
21580
- const resolvedCandidates = /* @__PURE__ */ new Set();
21581
- for (const target of findReExportTargetsForName(programRoot, exportedName)) {
21582
- const nextFilePath = resolveModulePath(filePath, target.source);
21628
+ for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
21629
+ const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
21583
21630
  if (!nextFilePath) continue;
21584
- const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
21585
- if (resolved) resolvedCandidates.add(resolved);
21631
+ const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
21632
+ if (resolved) return resolved;
21586
21633
  }
21587
- if (resolvedCandidates.size !== 1) return null;
21588
- return resolvedCandidates.values().next().value ?? null;
21634
+ return null;
21589
21635
  };
21590
21636
  const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
21591
21637
  const resolvedFilePath = resolveModulePath(fromFilename, source);
@@ -22370,26 +22416,6 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
22370
22416
  const importDeclaration = declarationNode.parent;
22371
22417
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22372
22418
  }));
22373
- const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
22374
- if (def.type !== "ImportBinding") return false;
22375
- const declarationNode = def.node;
22376
- if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
22377
- const importDeclaration = declarationNode.parent;
22378
- return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22379
- }));
22380
- const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
22381
- if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
22382
- const callee = stripParenExpression(declarator.init.callee);
22383
- if (isNodeOfType(callee, "Identifier")) {
22384
- const reference = getRef(analysis, callee);
22385
- if (!reference?.resolved) return callee.name === hookName;
22386
- return isReactNamedImportReference(reference, hookName);
22387
- }
22388
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
22389
- const namespaceReference = getRef(analysis, callee.object);
22390
- if (!namespaceReference?.resolved) return callee.object.name === "React";
22391
- return isReactNamespaceImportReference(namespaceReference);
22392
- };
22393
22419
  const isHookCallee$1 = (analysis, node, hookName) => {
22394
22420
  if (!node) return false;
22395
22421
  if (isNodeOfType(node, "Identifier")) {
@@ -22788,46 +22814,6 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
22788
22814
  "parseInt"
22789
22815
  ]);
22790
22816
  const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22791
- const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22792
- "isRawJSON",
22793
- "parse",
22794
- "rawJSON",
22795
- "stringify"
22796
- ])], ["Math", new Set([
22797
- "abs",
22798
- "acos",
22799
- "acosh",
22800
- "asin",
22801
- "asinh",
22802
- "atan",
22803
- "atan2",
22804
- "atanh",
22805
- "cbrt",
22806
- "ceil",
22807
- "clz32",
22808
- "cos",
22809
- "cosh",
22810
- "exp",
22811
- "floor",
22812
- "fround",
22813
- "hypot",
22814
- "imul",
22815
- "log",
22816
- "log10",
22817
- "log1p",
22818
- "log2",
22819
- "max",
22820
- "min",
22821
- "pow",
22822
- "round",
22823
- "sign",
22824
- "sin",
22825
- "sinh",
22826
- "sqrt",
22827
- "tan",
22828
- "tanh",
22829
- "trunc"
22830
- ])]]);
22831
22817
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22832
22818
  "concat",
22833
22819
  "filter",
@@ -22874,42 +22860,6 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
22874
22860
  }
22875
22861
  return parameter;
22876
22862
  };
22877
- const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
22878
- const isModuleFunction = (functionNode) => {
22879
- let ancestor = functionNode.parent;
22880
- while (ancestor) {
22881
- if (isFunctionLike$1(ancestor)) return false;
22882
- if (isNodeOfType(ancestor, "Program")) return true;
22883
- ancestor = ancestor.parent;
22884
- }
22885
- return false;
22886
- };
22887
- const getFunctionBindingNames = (functionNode) => {
22888
- const names = /* @__PURE__ */ new Set();
22889
- if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
22890
- const parent = functionNode.parent;
22891
- if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
22892
- return names;
22893
- };
22894
- const collectModuleBindingNames = (functionNode) => {
22895
- let program = functionNode.parent;
22896
- while (program && !isNodeOfType(program, "Program")) program = program.parent;
22897
- const bindingNames = /* @__PURE__ */ new Set();
22898
- if (!program || !isNodeOfType(program, "Program")) return bindingNames;
22899
- for (const statement of program.body ?? []) {
22900
- if (isNodeOfType(statement, "ImportDeclaration")) {
22901
- for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
22902
- continue;
22903
- }
22904
- const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
22905
- if (isNodeOfType(declaration, "VariableDeclaration")) {
22906
- for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
22907
- continue;
22908
- }
22909
- if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
22910
- }
22911
- return bindingNames;
22912
- };
22913
22863
  const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
22914
22864
  const substitutions = /* @__PURE__ */ new Map();
22915
22865
  const parameters = getFunctionParameters(functionNode);
@@ -23040,7 +22990,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
23040
22990
  for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
23041
22991
  return introducedBindings;
23042
22992
  };
23043
- const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
22993
+ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
23044
22994
  const effectFunction = getEffectFn(analysis, effectNode);
23045
22995
  if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
23046
22996
  const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
@@ -23049,8 +22999,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
23049
22999
  invocation: null,
23050
23000
  isDeferred: false,
23051
23001
  introducedBindings: /* @__PURE__ */ new Set(),
23052
- substitutions: /* @__PURE__ */ new Map(),
23053
- currentFilename
23002
+ substitutions: /* @__PURE__ */ new Map()
23054
23003
  };
23055
23004
  const frames = [rootFrame];
23056
23005
  walkAst(effectFunction, (child) => {
@@ -23071,8 +23020,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
23071
23020
  invocation: child,
23072
23021
  isDeferred,
23073
23022
  introducedBindings,
23074
- substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
23075
- currentFilename
23023
+ substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
23076
23024
  });
23077
23025
  };
23078
23026
  if (isFunctionLike$1(callee)) {
@@ -23125,174 +23073,6 @@ const isOpaqueHookCall = (callExpression) => {
23125
23073
  const calleeName = getCallCalleeName$1(callExpression);
23126
23074
  return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
23127
23075
  };
23128
- const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
23129
- let canContinue = true;
23130
- for (const statement of statements) {
23131
- if (!canContinue) {
23132
- if (!isNodeOfType(statement, "EmptyStatement")) return {
23133
- canContinue: false,
23134
- isValid: false
23135
- };
23136
- continue;
23137
- }
23138
- if (isNodeOfType(statement, "EmptyStatement")) continue;
23139
- if (isNodeOfType(statement, "ReturnStatement")) {
23140
- if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
23141
- canContinue: false,
23142
- isValid: false
23143
- };
23144
- canContinue = false;
23145
- continue;
23146
- }
23147
- if (isNodeOfType(statement, "BlockStatement")) {
23148
- const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
23149
- if (!blockSummary.isValid) return blockSummary;
23150
- canContinue = blockSummary.canContinue;
23151
- continue;
23152
- }
23153
- if (isNodeOfType(statement, "IfStatement")) {
23154
- if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
23155
- canContinue: false,
23156
- isValid: false
23157
- };
23158
- const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
23159
- if (!consequentSummary.isValid) return consequentSummary;
23160
- const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
23161
- canContinue: true,
23162
- isValid: true
23163
- };
23164
- if (!alternateSummary.isValid) return alternateSummary;
23165
- canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
23166
- continue;
23167
- }
23168
- return {
23169
- canContinue: false,
23170
- isValid: false
23171
- };
23172
- }
23173
- return {
23174
- canContinue,
23175
- isValid: true
23176
- };
23177
- };
23178
- const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
23179
- const node = stripParenExpression(expression);
23180
- if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
23181
- if (isNodeOfType(node, "Identifier")) {
23182
- const parameterIndex = environment.parameterIndices.get(node.name);
23183
- if (parameterIndex !== void 0) {
23184
- if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
23185
- return true;
23186
- }
23187
- return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
23188
- }
23189
- if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
23190
- if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
23191
- if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
23192
- if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
23193
- if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
23194
- return analyzeHelperExpression(property.value, environment, usedParameterIndices);
23195
- });
23196
- if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
23197
- if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23198
- if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
23199
- if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
23200
- if (isNodeOfType(node, "MemberExpression")) {
23201
- if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
23202
- return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
23203
- }
23204
- if (isFunctionLike$1(node)) {
23205
- if (isAsyncOrGeneratorFunction(node)) return false;
23206
- const callbackParameterIndices = new Map(environment.parameterIndices);
23207
- for (const parameter of getFunctionParameters(node)) {
23208
- if (!isNodeOfType(parameter, "Identifier")) return false;
23209
- callbackParameterIndices.set(parameter.name, null);
23210
- }
23211
- const callbackEnvironment = {
23212
- parameterIndices: callbackParameterIndices,
23213
- recursiveNames: environment.recursiveNames,
23214
- shadowedGlobalNames: environment.shadowedGlobalNames
23215
- };
23216
- if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
23217
- const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23218
- return callbackSummary.isValid && !callbackSummary.canContinue;
23219
- }
23220
- if (isNodeOfType(node, "CallExpression")) {
23221
- const callee = stripParenExpression(node.callee);
23222
- const calleeRoot = getMemberRoot(callee);
23223
- 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);
23224
- const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
23225
- const namespaceMemberName = getStaticMemberName(callee);
23226
- const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
23227
- const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23228
- if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
23229
- if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
23230
- return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23231
- }
23232
- if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23233
- return false;
23234
- };
23235
- const helperSummaryCache = /* @__PURE__ */ new WeakMap();
23236
- const summarizeHelperReturn = (functionNode) => {
23237
- if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
23238
- if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
23239
- if (isAsyncOrGeneratorFunction(functionNode)) {
23240
- helperSummaryCache.set(functionNode, null);
23241
- return null;
23242
- }
23243
- const parameterIndices = /* @__PURE__ */ new Map();
23244
- const parameters = getFunctionParameters(functionNode);
23245
- for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
23246
- const parameter = parameters[parameterIndex];
23247
- if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
23248
- helperSummaryCache.set(functionNode, null);
23249
- return null;
23250
- }
23251
- parameterIndices.set(parameter.name, parameterIndex);
23252
- }
23253
- const environment = {
23254
- parameterIndices,
23255
- recursiveNames: getFunctionBindingNames(functionNode),
23256
- shadowedGlobalNames: collectModuleBindingNames(functionNode)
23257
- };
23258
- const usedParameterIndices = /* @__PURE__ */ new Set();
23259
- if (!isNodeOfType(functionNode.body, "BlockStatement")) {
23260
- if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
23261
- helperSummaryCache.set(functionNode, null);
23262
- return null;
23263
- }
23264
- } else {
23265
- const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
23266
- if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
23267
- helperSummaryCache.set(functionNode, null);
23268
- return null;
23269
- }
23270
- }
23271
- const summary = { usedParameterIndices };
23272
- helperSummaryCache.set(functionNode, summary);
23273
- return summary;
23274
- };
23275
- const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
23276
- if (!isNodeOfType(callee, "Identifier")) return null;
23277
- const reference = getRef(analysis, callee);
23278
- if (!reference?.resolved) return null;
23279
- const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
23280
- if (importDefinition) {
23281
- if (!currentFilename) return null;
23282
- const specifier = importDefinition.node;
23283
- if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
23284
- const importDeclaration = specifier.parent;
23285
- if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
23286
- if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
23287
- const source = importDeclaration.source?.value;
23288
- if (typeof source !== "string") return null;
23289
- const exportedName = resolveImportedExportName(specifier);
23290
- if (!exportedName) return null;
23291
- return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
23292
- }
23293
- const callable = resolveWrappedCallable(analysis, callee);
23294
- return callable && isModuleFunction(callable) ? callable : null;
23295
- };
23296
23076
  const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
23297
23077
  const definitionNode = definition.node;
23298
23078
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
@@ -23378,55 +23158,38 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23378
23158
  const calleeRoot = getMemberRoot(callee);
23379
23159
  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;
23380
23160
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23381
- if (isPureGlobalCall || isPureMemberTransform) {
23382
- if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
23383
- for (const argument of node.arguments ?? []) {
23384
- if (isFunctionLike$1(argument)) continue;
23385
- mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23386
- }
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;
23387
23169
  return evidence;
23388
23170
  }
23389
23171
  if (remainingCallFrames <= 0) {
23390
23172
  evidence.hasUnknownSource = true;
23391
23173
  return evidence;
23392
23174
  }
23393
- const localHelperFunction = resolveWrappedCallable(analysis, callee);
23394
- if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
23395
- if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
23396
- evidence.hasUnknownSource = true;
23397
- return evidence;
23398
- }
23399
- const localHelperFrame = {
23400
- functionNode: localHelperFunction,
23401
- invocation: node,
23402
- isDeferred: false,
23403
- introducedBindings: /* @__PURE__ */ new Set(),
23404
- substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
23405
- currentFilename: frame.currentFilename
23406
- };
23407
- const returnedExpressions = getReturnedExpressions(localHelperFunction);
23408
- if (returnedExpressions.length === 0) {
23409
- evidence.hasUnknownSource = true;
23410
- return evidence;
23411
- }
23412
- for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
23175
+ const callable = resolveWrappedCallable(analysis, callee);
23176
+ if (!callable || callable.async === true || functionInvokesItself(analysis, callable)) {
23177
+ evidence.hasUnknownSource = true;
23413
23178
  return evidence;
23414
23179
  }
23415
- const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
23416
- const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
23417
- if (!helperSummary) {
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) {
23418
23189
  evidence.hasUnknownSource = true;
23419
23190
  return evidence;
23420
23191
  }
23421
- const argumentsForHelper = node.arguments ?? [];
23422
- for (const parameterIndex of helperSummary.usedParameterIndices) {
23423
- const argument = argumentsForHelper[parameterIndex];
23424
- if (!argument) {
23425
- evidence.hasUnknownSource = true;
23426
- return evidence;
23427
- }
23428
- mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
23429
- }
23192
+ for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, valueFrame, remainingCallFrames - 1, new Set(visitedBindings)));
23430
23193
  return evidence;
23431
23194
  }
23432
23195
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
@@ -23444,20 +23207,6 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23444
23207
  }
23445
23208
  return evidence;
23446
23209
  };
23447
- const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
23448
- const evidence = collectValueEvidence(analysis, expression, {
23449
- functionNode: componentFunction,
23450
- invocation: null,
23451
- isDeferred: false,
23452
- introducedBindings: /* @__PURE__ */ new Set(),
23453
- substitutions: /* @__PURE__ */ new Map(),
23454
- currentFilename
23455
- }, 1);
23456
- return {
23457
- sourceReferences: evidence.sourceReferences,
23458
- isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
23459
- };
23460
- };
23461
23210
  const findStateSetterReference = (analysis, callExpression) => {
23462
23211
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
23463
23212
  const callee = stripParenExpression(callExpression.callee);
@@ -23520,8 +23269,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
23520
23269
  }
23521
23270
  return false;
23522
23271
  };
23523
- const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
23524
- const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
23272
+ const collectEffectStateWriteFacts = (analysis, effectNode) => {
23273
+ const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
23525
23274
  if (frames.length === 0) return [];
23526
23275
  const effectHasCleanup = hasCleanup(analysis, effectNode);
23527
23276
  const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
@@ -23541,8 +23290,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
23541
23290
  invocation: callExpression,
23542
23291
  isDeferred: frame.isDeferred,
23543
23292
  introducedBindings: collectIntroducedBindings(analysis, writtenValue),
23544
- substitutions: /* @__PURE__ */ new Map(),
23545
- currentFilename
23293
+ substitutions: /* @__PURE__ */ new Map()
23546
23294
  };
23547
23295
  valueEvidence = emptyEvidence();
23548
23296
  const returnedExpressions = getReturnedExpressions(writtenValue);
@@ -23591,7 +23339,7 @@ const noAdjustStateOnPropChange = defineRule({
23591
23339
  const dependencyReferences = getEffectDepsRefs(analysis, node);
23592
23340
  if (!dependencyReferences) return;
23593
23341
  if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
23594
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
23342
+ for (const fact of collectEffectStateWriteFacts(analysis, node)) {
23595
23343
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
23596
23344
  context.report({
23597
23345
  node: fact.callExpression,
@@ -24852,139 +24600,6 @@ const createRelativeImportSource = (filename, targetFilePath) => {
24852
24600
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
24853
24601
  };
24854
24602
  //#endregion
24855
- //#region src/plugin/utils/is-barrel-index-module.ts
24856
- const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
24857
- const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24858
- const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24859
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24860
- const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
24861
- const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
24862
- const createNonBarrelInfo = () => ({
24863
- isBarrel: false,
24864
- exportsByName: /* @__PURE__ */ new Map(),
24865
- starExportSources: []
24866
- });
24867
- const addImportedBinding = (importedBindings, binding) => {
24868
- importedBindings.set(binding.localName, {
24869
- ...binding,
24870
- didExport: false
24871
- });
24872
- };
24873
- const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
24874
- for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
24875
- localName: specifier.exportedName,
24876
- importedName: specifier.localName,
24877
- source,
24878
- isTypeOnly: specifier.isTypeOnly
24879
- });
24880
- };
24881
- const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
24882
- const trimmedImportClause = importClause.trim();
24883
- const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
24884
- if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
24885
- localName: namespaceMatch[1],
24886
- importedName: "*",
24887
- source,
24888
- isTypeOnly: declarationIsTypeOnly
24889
- });
24890
- const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
24891
- if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
24892
- const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
24893
- if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
24894
- localName: defaultImportName,
24895
- importedName: "default",
24896
- source,
24897
- isTypeOnly: declarationIsTypeOnly
24898
- });
24899
- };
24900
- const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
24901
- let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
24902
- collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
24903
- return "";
24904
- });
24905
- withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
24906
- const isTypeOnly = Boolean(typeKeyword);
24907
- if (namespaceExportName) {
24908
- exportsByName.set(namespaceExportName, {
24909
- exportedName: namespaceExportName,
24910
- importedName: "*",
24911
- source,
24912
- isTypeOnly
24913
- });
24914
- return "";
24915
- }
24916
- if (specifiersText) {
24917
- for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
24918
- exportedName: specifier.exportedName,
24919
- importedName: specifier.localName,
24920
- source,
24921
- isTypeOnly: specifier.isTypeOnly
24922
- });
24923
- return "";
24924
- }
24925
- starExportSources.push(source);
24926
- return "";
24927
- });
24928
- withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
24929
- for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
24930
- const importedBinding = importedBindings.get(specifier.localName);
24931
- if (!importedBinding) return _match;
24932
- importedBinding.didExport = true;
24933
- exportsByName.set(specifier.exportedName, {
24934
- exportedName: specifier.exportedName,
24935
- importedName: importedBinding.importedName,
24936
- source: importedBinding.source,
24937
- isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
24938
- });
24939
- }
24940
- return "";
24941
- });
24942
- return withoutKnownDeclarations;
24943
- };
24944
- const hasUnexportedRuntimeImport = (importedBindings) => {
24945
- for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
24946
- return false;
24947
- };
24948
- const classifyBarrelModule = (sourceText) => {
24949
- const strippedSource = stripJsComments(sourceText).trim();
24950
- if (!strippedSource) return createNonBarrelInfo();
24951
- const importedBindings = /* @__PURE__ */ new Map();
24952
- const exportsByName = /* @__PURE__ */ new Map();
24953
- const starExportSources = [];
24954
- if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
24955
- return {
24956
- isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
24957
- exportsByName,
24958
- starExportSources
24959
- };
24960
- };
24961
- const getBarrelIndexModuleInfo = (filePath) => {
24962
- if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
24963
- recordContentProbe(filePath);
24964
- let fileStat;
24965
- try {
24966
- fileStat = fs.statSync(filePath);
24967
- } catch {
24968
- fileStat = null;
24969
- }
24970
- const cachedResult = barrelIndexModuleInfoCache.get(filePath);
24971
- if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
24972
- if (fileStat === null) return createNonBarrelInfo();
24973
- let moduleInfo = createNonBarrelInfo();
24974
- try {
24975
- moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
24976
- } catch {
24977
- moduleInfo = createNonBarrelInfo();
24978
- }
24979
- barrelIndexModuleInfoCache.set(filePath, {
24980
- mtimeMs: fileStat.mtimeMs,
24981
- size: fileStat.size,
24982
- moduleInfo
24983
- });
24984
- return moduleInfo;
24985
- };
24986
- const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
24987
- //#endregion
24988
24603
  //#region src/react-native-dependency-names.ts
24989
24604
  const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
24990
24605
  "expo",
@@ -25187,35 +24802,6 @@ const classifyReactNativeFileTarget = (context) => {
25187
24802
  };
25188
24803
  const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
25189
24804
  //#endregion
25190
- //#region src/plugin/utils/resolve-barrel-export-file-path.ts
25191
- const getUniqueFilePath = (filePaths) => {
25192
- const uniqueFilePaths = new Set(filePaths);
25193
- if (uniqueFilePaths.size !== 1) return null;
25194
- const [filePath] = uniqueFilePaths;
25195
- return filePath ?? null;
25196
- };
25197
- const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
25198
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
25199
- if (!resolvedTargetPath) return null;
25200
- const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
25201
- if (nestedTargetPath) return nestedTargetPath;
25202
- return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
25203
- };
25204
- const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
25205
- if (visitedFilePaths.has(barrelFilePath)) return null;
25206
- visitedFilePaths.add(barrelFilePath);
25207
- const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
25208
- if (!moduleInfo.isBarrel) return null;
25209
- const target = moduleInfo.exportsByName.get(exportedName);
25210
- if (target) {
25211
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
25212
- if (!resolvedTargetPath) return null;
25213
- return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
25214
- }
25215
- if (exportedName === "default") return null;
25216
- return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
25217
- };
25218
- //#endregion
25219
24805
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
25220
24806
  const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
25221
24807
  const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
@@ -26510,6 +26096,57 @@ const noDefaultProps = defineRule({
26510
26096
  } })
26511
26097
  });
26512
26098
  //#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
26513
26150
  //#region src/plugin/utils/is-component-function.ts
26514
26151
  const isFunctionAssignedToComponent = (functionNode) => {
26515
26152
  let cursor = functionNode.parent ?? null;
@@ -26643,236 +26280,6 @@ const createComponentPropStackTracker = (callbacks) => {
26643
26280
  };
26644
26281
  };
26645
26282
  //#endregion
26646
- //#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
26647
- const findReferenceDeclarator = (reference) => {
26648
- for (const definition of reference.resolved?.defs ?? []) {
26649
- const definitionNode = definition.node;
26650
- if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
26651
- }
26652
- return null;
26653
- };
26654
- const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
26655
- const node = stripParenExpression(expression);
26656
- if (!isNodeOfType(node, "Identifier")) return null;
26657
- const reference = getRef(analysis, node);
26658
- if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
26659
- if (isProp(analysis, reference)) {
26660
- if (isWholePropsObjectReference(analysis, reference)) return null;
26661
- return { bindingIdentity: reference.resolved };
26662
- }
26663
- if (isState(analysis, reference)) {
26664
- const stateDeclarator = getUseStateDecl(analysis, reference);
26665
- if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
26666
- return { bindingIdentity: reference.resolved };
26667
- }
26668
- if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
26669
- const declarator = findReferenceDeclarator(reference);
26670
- if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
26671
- const nextVisitedBindings = new Set(visitedBindings);
26672
- nextVisitedBindings.add(reference.resolved);
26673
- return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
26674
- };
26675
- const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
26676
- const getDirectBranchStatements = (branch) => {
26677
- if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
26678
- return [branch];
26679
- };
26680
- const getDirectCallExpression = (statement) => {
26681
- if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26682
- const expression = stripParenExpression(statement.expression);
26683
- return isNodeOfType(expression, "CallExpression") ? expression : null;
26684
- };
26685
- const getDirectAssignmentExpression = (statement) => {
26686
- if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26687
- const expression = stripParenExpression(statement.expression);
26688
- return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
26689
- };
26690
- const findDirectStateSetterReference = (analysis, callExpression) => {
26691
- if (!isNodeOfType(callExpression, "CallExpression")) return null;
26692
- const callee = stripParenExpression(callExpression.callee);
26693
- if (!isNodeOfType(callee, "Identifier")) return null;
26694
- const reference = getRef(analysis, callee);
26695
- return reference && isStateSetter(analysis, reference) ? reference : null;
26696
- };
26697
- const getStaticRefCurrentReference = (analysis, expression) => {
26698
- const node = stripParenExpression(expression);
26699
- if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
26700
- const reference = getRef(analysis, node.object);
26701
- if (!reference) return null;
26702
- const declarator = findReferenceDeclarator(reference);
26703
- return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
26704
- };
26705
- const getStateTracker = (analysis, expression) => {
26706
- const node = stripParenExpression(expression);
26707
- if (!isNodeOfType(node, "Identifier")) return null;
26708
- const reference = getRef(analysis, node);
26709
- if (!reference || !isState(analysis, reference)) return null;
26710
- const declarator = getUseStateDecl(analysis, reference);
26711
- if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
26712
- return {
26713
- kind: "state",
26714
- declarator
26715
- };
26716
- };
26717
- const getRefTracker = (analysis, expression) => {
26718
- const reference = getStaticRefCurrentReference(analysis, expression);
26719
- return reference ? {
26720
- kind: "ref",
26721
- reference
26722
- } : null;
26723
- };
26724
- const getTrackerInitializer = (tracker) => {
26725
- const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
26726
- if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
26727
- const initializer = declarator.init.arguments?.[0];
26728
- return initializer ? initializer : null;
26729
- };
26730
- const isTrackerSynchronized = (analysis, guard) => {
26731
- for (const statement of guard.statements) {
26732
- if (guard.tracker.kind === "state") {
26733
- const callExpression = getDirectCallExpression(statement);
26734
- if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26735
- const setterReference = findDirectStateSetterReference(analysis, callExpression);
26736
- if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
26737
- const writtenValue = callExpression.arguments?.[0];
26738
- if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
26739
- continue;
26740
- }
26741
- const assignmentExpression = getDirectAssignmentExpression(statement);
26742
- if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
26743
- if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
26744
- if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
26745
- }
26746
- return false;
26747
- };
26748
- const parseRenderTrackerGuard = (analysis, statement) => {
26749
- if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
26750
- const left = statement.test.left;
26751
- const right = statement.test.right;
26752
- const candidates = [{
26753
- sourceExpression: left,
26754
- trackerExpression: right
26755
- }, {
26756
- sourceExpression: right,
26757
- trackerExpression: left
26758
- }];
26759
- for (const candidate of candidates) {
26760
- const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
26761
- if (!source) continue;
26762
- const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
26763
- if (!tracker) continue;
26764
- const trackerInitializer = getTrackerInitializer(tracker);
26765
- if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
26766
- const guard = {
26767
- source,
26768
- tracker,
26769
- statements: getDirectBranchStatements(statement.consequent)
26770
- };
26771
- return isTrackerSynchronized(analysis, guard) ? guard : null;
26772
- }
26773
- return null;
26774
- };
26775
- const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
26776
- if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
26777
- const references = setterReference.resolved.references.filter((reference) => !reference.init);
26778
- if (references.length !== 1) return false;
26779
- return references[0].identifier === callExpression.callee;
26780
- };
26781
- const getStateInitializer = (stateDeclarator) => {
26782
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
26783
- const initializer = stateDeclarator.init.arguments?.[0];
26784
- return initializer ? initializer : null;
26785
- };
26786
- const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
26787
- if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
26788
- const componentFunction = componentBody.parent;
26789
- const facts = [];
26790
- for (const statement of componentBody.body ?? []) {
26791
- const guard = parseRenderTrackerGuard(analysis, statement);
26792
- if (!guard) continue;
26793
- for (const branchStatement of guard.statements) {
26794
- const callExpression = getDirectCallExpression(branchStatement);
26795
- if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26796
- const setterReference = findDirectStateSetterReference(analysis, callExpression);
26797
- if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
26798
- const stateDeclarator = getUseStateDecl(analysis, setterReference);
26799
- if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
26800
- const writtenValue = callExpression.arguments?.[0];
26801
- const initializer = getStateInitializer(stateDeclarator);
26802
- if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
26803
- facts.push({
26804
- callExpression,
26805
- stateDeclarator
26806
- });
26807
- }
26808
- }
26809
- return facts;
26810
- };
26811
- //#endregion
26812
- //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26813
- const getStateName$1 = (stateDeclarator) => {
26814
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26815
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26816
- const stateBinding = stateDeclarator.id.elements?.[0];
26817
- if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26818
- const setterBinding = stateDeclarator.id.elements?.[1];
26819
- if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26820
- if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26821
- return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26822
- };
26823
- const noDerivedState = defineRule({
26824
- id: "no-derived-state",
26825
- title: "Derived value copied into state",
26826
- severity: "warn",
26827
- tags: ["test-noise"],
26828
- 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",
26829
- create: (context) => {
26830
- const reportStateWrite = (callExpression, stateDeclarator) => {
26831
- const stateName = getStateName$1(stateDeclarator);
26832
- context.report({
26833
- node: callExpression,
26834
- message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26835
- });
26836
- };
26837
- return {
26838
- ...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
26839
- if (!componentBody) return;
26840
- const analysis = getProgramAnalysis(componentBody);
26841
- if (!analysis) return;
26842
- for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
26843
- } }).visitors,
26844
- CallExpression(node) {
26845
- if (!isUseEffect(node)) return;
26846
- const analysis = getProgramAnalysis(node);
26847
- if (!analysis) return;
26848
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
26849
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26850
- reportStateWrite(fact.callExpression, fact.stateDeclarator);
26851
- }
26852
- }
26853
- };
26854
- }
26855
- });
26856
- //#endregion
26857
- //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26858
- const noDerivedStateEffect = defineRule({
26859
- id: "no-derived-state-effect",
26860
- title: "Derived state stored in an effect",
26861
- severity: "warn",
26862
- tags: ["test-noise"],
26863
- 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",
26864
- create: (context) => ({ CallExpression(node) {
26865
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26866
- const analysis = getProgramAnalysis(node);
26867
- if (!analysis) return;
26868
- if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26869
- context.report({
26870
- node,
26871
- message: "You pay an extra render for state you can derive from other values."
26872
- });
26873
- } })
26874
- });
26875
- //#endregion
26876
26283
  //#region src/plugin/utils/is-initial-only-prop-name.ts
26877
26284
  const isInitialOnlyPropName = (propName) => {
26878
26285
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -30236,26 +29643,28 @@ const noGiantComponent = defineRule({
30236
29643
  const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
30237
29644
  return lineCount > 300 ? lineCount : null;
30238
29645
  };
30239
- const reportOversizedComponent = (nameNode, componentName) => {
29646
+ const reportOversizedComponent = (nameNode, componentName, lineCount) => {
30240
29647
  context.report({
30241
29648
  node: nameNode,
30242
- message: `Component "${componentName}" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.`
29649
+ message: `Component "${componentName}" is ${lineCount} lines long, which is hard to read & change. Split it into a few smaller components.`
30243
29650
  });
30244
29651
  };
30245
29652
  return {
30246
29653
  FunctionDeclaration(node) {
30247
29654
  if (!node.id?.name || !isUppercaseName(node.id.name)) return;
30248
- if (getOversizedComponentLineCount(node) === null) return;
29655
+ const lineCount = getOversizedComponentLineCount(node);
29656
+ if (lineCount === null) return;
30249
29657
  if (!functionContainsReactRenderOutput(node, context.scopes)) return;
30250
- reportOversizedComponent(node.id, node.id.name);
29658
+ reportOversizedComponent(node.id, node.id.name, lineCount);
30251
29659
  },
30252
29660
  VariableDeclarator(node) {
30253
29661
  if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
30254
29662
  const functionNode = unwrapReactHocFunction(node.init);
30255
29663
  if (!functionNode) return;
30256
- if (getOversizedComponentLineCount(functionNode) === null) return;
29664
+ const lineCount = getOversizedComponentLineCount(functionNode);
29665
+ if (lineCount === null) return;
30257
29666
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
30258
- reportOversizedComponent(node.id, node.id.name);
29667
+ reportOversizedComponent(node.id, node.id.name, lineCount);
30259
29668
  }
30260
29669
  };
30261
29670
  }
@@ -30892,131 +30301,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
30892
30301
  } })
30893
30302
  });
30894
30303
  //#endregion
30895
- //#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
30896
- const TIMER_FUNCTION_NAMES = new Set([
30897
- "cancelAnimationFrame",
30898
- "clearInterval",
30899
- "clearTimeout",
30900
- "queueMicrotask",
30901
- "requestAnimationFrame",
30902
- "setInterval",
30903
- "setTimeout"
30904
- ]);
30905
- const STORAGE_MUTATION_METHOD_NAMES = new Set([
30906
- "clear",
30907
- "removeItem",
30908
- "setItem"
30909
- ]);
30910
- const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
30911
- const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
30912
- const NOTIFICATION_RECEIVER_NAMES = new Set([
30913
- "message",
30914
- "notification",
30915
- "toast"
30916
- ]);
30917
- const NOTIFICATION_METHOD_NAMES = new Set([
30918
- "error",
30919
- "info",
30920
- "loading",
30921
- "open",
30922
- "show",
30923
- "success",
30924
- "warning"
30925
- ]);
30926
- const getMemberCall = (node) => {
30927
- if (!isNodeOfType(node, "CallExpression")) return null;
30928
- if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
30929
- if (!isNodeOfType(node.callee.property, "Identifier")) return null;
30930
- return {
30931
- methodName: node.callee.property.name,
30932
- receiver: stripParenExpression(node.callee.object)
30933
- };
30934
- };
30935
- const isNotificationReceiver = (receiver, scopes) => {
30936
- if (!isNodeOfType(receiver, "Identifier")) return false;
30937
- if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
30938
- const symbol = scopes.symbolFor(receiver);
30939
- if (symbol?.kind === "import") return true;
30940
- if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
30941
- const callee = symbol.initializer.callee;
30942
- return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
30943
- };
30944
- const getKnownImpureCall = (callExpression, scopes) => {
30945
- if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
30946
- const memberCall = getMemberCall(callExpression);
30947
- if (!memberCall) return null;
30948
- const { methodName, receiver } = memberCall;
30949
- if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
30950
- if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
30951
- if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
30952
- return null;
30953
- };
30954
- const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
30955
- let rootIdentifier = null;
30956
- if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
30957
- else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
30958
- if (!rootIdentifier) return null;
30959
- const updaterScope = scopes.ownScopeFor(updater);
30960
- if (!updaterScope) return null;
30961
- const symbol = scopes.symbolFor(rootIdentifier);
30962
- if (!symbol) return `the external value "${rootIdentifier.name}"`;
30963
- if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
30964
- return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
30965
- };
30966
- const findImpureUpdaterOperation = (updater, scopes) => {
30967
- const analysis = getProgramAnalysis(updater);
30968
- let operation = null;
30969
- walkAst(updater, (child) => {
30970
- if (operation) return false;
30971
- if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
30972
- if (isNodeOfType(child, "CallExpression")) {
30973
- if (isNodeOfType(child.callee, "Identifier") && analysis) {
30974
- const calleeReference = getRef(analysis, child.callee);
30975
- if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
30976
- operation = `the nested state update "${child.callee.name}()"`;
30977
- return false;
30978
- }
30979
- }
30980
- const impureCall = getKnownImpureCall(child, scopes);
30981
- if (impureCall) {
30982
- operation = impureCall;
30983
- return false;
30984
- }
30985
- }
30986
- if (isNodeOfType(child, "AssignmentExpression")) {
30987
- operation = getExternalAssignmentDescription(child.left, updater, scopes);
30988
- if (operation) return false;
30989
- }
30990
- if (isNodeOfType(child, "UpdateExpression")) {
30991
- operation = getExternalAssignmentDescription(child.argument, updater, scopes);
30992
- if (operation) return false;
30993
- }
30994
- });
30995
- return operation;
30996
- };
30997
- const noImpureStateUpdater = defineRule({
30998
- id: "no-impure-state-updater",
30999
- title: "State updater has side effects",
31000
- severity: "error",
31001
- 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.",
31002
- create: (context) => ({ CallExpression(node) {
31003
- const updater = node.arguments?.[0];
31004
- if (!updater || !isFunctionLike$1(updater)) return;
31005
- const analysis = getProgramAnalysis(node);
31006
- if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
31007
- const calleeReference = getRef(analysis, node.callee);
31008
- if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
31009
- const stateDeclarator = getUseStateDecl(analysis, calleeReference);
31010
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
31011
- const operation = findImpureUpdaterOperation(updater, context.scopes);
31012
- if (!operation) return;
31013
- context.report({
31014
- node: updater,
31015
- 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.`
31016
- });
31017
- } })
31018
- });
31019
- //#endregion
31020
30304
  //#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
31021
30305
  const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
31022
30306
  const REACT_USE_REF_OPTIONS = {
@@ -31183,7 +30467,7 @@ const noInitializeState = defineRule({
31183
30467
  if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
31184
30468
  const analysis = getProgramAnalysis(node);
31185
30469
  if (!analysis) return;
31186
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
30470
+ for (const fact of collectEffectStateWriteFacts(analysis, node)) {
31187
30471
  if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
31188
30472
  const stateName = getStateName(fact.stateDeclarator);
31189
30473
  context.report({
@@ -34185,166 +33469,6 @@ const isDirectParentCallbackRef = (analysis, ref) => {
34185
33469
  return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
34186
33470
  }));
34187
33471
  };
34188
- const getDeclarationKind = (declarator) => {
34189
- const declaration = declarator.parent;
34190
- return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
34191
- };
34192
- const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
34193
- const getDestructuredPropertyName = (bindingIdentifier) => {
34194
- const property = bindingIdentifier.parent;
34195
- if (!property || !isNodeOfType(property, "Property")) return null;
34196
- if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
34197
- return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
34198
- };
34199
- const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
34200
- const unwrappedExpression = stripParenExpression(expression);
34201
- if (isNodeOfType(unwrappedExpression, "Identifier")) {
34202
- const callbackReference = getRef(analysis, unwrappedExpression);
34203
- const callbackVariable = callbackReference?.resolved;
34204
- if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
34205
- if (isProp(analysis, callbackReference)) {
34206
- if (isWholePropsObjectReference(analysis, callbackReference)) return null;
34207
- const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
34208
- return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
34209
- }
34210
- if (hasMutableBindingWrite(callbackReference)) return null;
34211
- const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
34212
- if (definitions.length !== 1) return null;
34213
- const declarator = definitions[0];
34214
- if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
34215
- visitedVariables.add(callbackVariable);
34216
- if (isNodeOfType(declarator.id, "ObjectPattern")) {
34217
- const bindingIdentifier = callbackVariable.defs[0]?.name;
34218
- const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
34219
- const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
34220
- return propertyName && propsReference ? propertyName : null;
34221
- }
34222
- if (!isNodeOfType(declarator.id, "Identifier")) return null;
34223
- return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
34224
- }
34225
- if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
34226
- const callbackName = getStaticMemberPropertyName(unwrappedExpression);
34227
- if (!callbackName) return null;
34228
- const receiver = stripParenExpression(unwrappedExpression.object);
34229
- if (!isNodeOfType(receiver, "Identifier")) return null;
34230
- const receiverReference = getRef(analysis, receiver);
34231
- if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
34232
- return callbackName;
34233
- };
34234
- const isNullishRefInitializer = (initializer) => {
34235
- if (!initializer) return true;
34236
- const unwrappedInitializer = stripParenExpression(initializer);
34237
- if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
34238
- if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
34239
- return unwrappedInitializer.name === "undefined";
34240
- };
34241
- const getDirectComponentBodyStatement = (node, componentBody) => {
34242
- let current = node;
34243
- while (current?.parent && current.parent !== componentBody) current = current.parent;
34244
- return current?.parent === componentBody ? current : null;
34245
- };
34246
- const getVariableForDeclarator = (analysis, declarator) => {
34247
- for (const scope of analysis.scopeManager.scopes) {
34248
- const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
34249
- if (variable) return variable;
34250
- }
34251
- return null;
34252
- };
34253
- const getRefMember = (identifier) => {
34254
- const receiver = findTransparentExpressionRoot(identifier);
34255
- const member = receiver.parent;
34256
- if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
34257
- return member;
34258
- };
34259
- const getRefMemberAssignment = (identifier) => {
34260
- const member = getRefMember(identifier);
34261
- if (!member) return null;
34262
- const memberRoot = findTransparentExpressionRoot(member);
34263
- const assignment = memberRoot.parent;
34264
- if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
34265
- return assignment;
34266
- };
34267
- const getRefAliasDeclarator = (identifier) => {
34268
- const initializer = findTransparentExpressionRoot(identifier);
34269
- const declarator = initializer.parent;
34270
- if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
34271
- return declarator;
34272
- };
34273
- const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
34274
- if (!isNodeOfType(receiver, "Identifier")) return null;
34275
- const receiverReference = getRef(analysis, receiver);
34276
- if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
34277
- const variables = /* @__PURE__ */ new Set();
34278
- let currentVariable = receiverReference.resolved;
34279
- let refCall = null;
34280
- while (currentVariable && !variables.has(currentVariable)) {
34281
- variables.add(currentVariable);
34282
- const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
34283
- if (definitions.length !== 1) return null;
34284
- const declarator = definitions[0];
34285
- if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
34286
- if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
34287
- refCall = declarator.init;
34288
- break;
34289
- }
34290
- if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
34291
- const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
34292
- if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
34293
- currentVariable = upstreamReference.resolved;
34294
- }
34295
- if (!refCall) return null;
34296
- const pendingVariables = [...variables];
34297
- while (pendingVariables.length > 0) {
34298
- const variable = pendingVariables.pop();
34299
- if (!variable) continue;
34300
- for (const candidateReference of variable.references) {
34301
- const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
34302
- if (!aliasDeclarator) continue;
34303
- const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
34304
- if (!aliasVariable || variables.has(aliasVariable)) continue;
34305
- if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
34306
- variables.add(aliasVariable);
34307
- pendingVariables.push(aliasVariable);
34308
- }
34309
- }
34310
- return {
34311
- refCall,
34312
- variables
34313
- };
34314
- };
34315
- const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
34316
- const callee = stripParenExpression(callExpression.callee);
34317
- if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
34318
- const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
34319
- if (!bindingProvenance) return null;
34320
- const { refCall, variables } = bindingProvenance;
34321
- const componentFunction = findEnclosingFunction(effectCall);
34322
- if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
34323
- if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
34324
- const callbackPropNames = /* @__PURE__ */ new Set();
34325
- const initializer = refCall.arguments?.[0];
34326
- const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
34327
- if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
34328
- else if (!isNullishRefInitializer(initializer)) return null;
34329
- for (const variable of variables) for (const candidateReference of variable.references) {
34330
- if (candidateReference.init) continue;
34331
- if (candidateReference.isWrite()) return null;
34332
- const identifier = candidateReference.identifier;
34333
- if (getRefAliasDeclarator(identifier)) continue;
34334
- const member = getRefMember(identifier);
34335
- if (!member || getStaticMemberPropertyName(member) !== "current") return null;
34336
- const memberParent = findTransparentExpressionRoot(member).parent;
34337
- if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
34338
- const assignment = getRefMemberAssignment(identifier);
34339
- if (!assignment) continue;
34340
- const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
34341
- if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
34342
- const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
34343
- if (!assignedCallbackName) return null;
34344
- callbackPropNames.add(assignedCallbackName);
34345
- }
34346
- return callbackPropNames.size > 0 ? { callbackPropNames } : null;
34347
- };
34348
33472
  const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
34349
33473
  const node = def.node;
34350
33474
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
@@ -34400,79 +33524,71 @@ const noPassDataToParent = defineRule({
34400
33524
  severity: "warn",
34401
33525
  tags: ["test-noise"],
34402
33526
  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",
34403
- create: (context) => {
34404
- const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
34405
- allowGlobalReactNamespace: true,
34406
- allowUnboundBareCalls: true
34407
- });
34408
- return { CallExpression(node) {
34409
- if (!isUseEffect(node)) return;
34410
- const analysis = getProgramAnalysis(node);
34411
- if (!analysis) return;
34412
- if (hasCleanup(analysis, node)) return;
34413
- const effectFnRefs = getEffectFnRefs(analysis, node);
34414
- if (!effectFnRefs) return;
34415
- const effectFn = getEffectFn(analysis, node);
34416
- if (!effectFn) return;
34417
- for (const ref of effectFnRefs) {
34418
- const callExpr = getCallExpr(ref);
34419
- if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
34420
- const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
34421
- if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
34422
- if (!isSynchronous(ref.identifier, effectFn)) continue;
34423
- const calleeNode = unwrapChainExpression$1(callExpr.callee);
34424
- const identifier = ref.identifier;
34425
- if (callbackRefProvenance) {
34426
- if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
34427
- } else if (calleeNode === identifier) {
34428
- if (!isDirectParentCallbackRef(analysis, ref)) continue;
34429
- if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
34430
- } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
34431
- if (!isWholePropsObjectReference(analysis, ref)) continue;
34432
- if (isCustomHookParameter(ref)) continue;
34433
- } else continue;
34434
- const methodName = getCallMethodName(calleeNode);
34435
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
34436
- if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
34437
- if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
34438
- if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
34439
- 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) ?? ""));
34440
- const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
34441
- const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
34442
- if (isFunctionLike$1(argument)) {
34443
- if (!isSetterNamedCallee) return [];
34444
- return getFunctionalUpdaterDataRefs(analysis, argument);
34445
- }
34446
- if (isHandlerBagArgument(analysis, argument)) return [];
34447
- if (isParentWiredHookResultArgument(analysis, argument)) return [];
34448
- if (isNodeOfType(argument, "Identifier")) {
34449
- const argumentRef = getRef(analysis, argument);
34450
- if (argumentRef && resolveToFunction(argumentRef)) return [];
34451
- }
34452
- return getDownstreamRefs(analysis, argument);
34453
- }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
34454
- if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
34455
- if (!argsUpstreamRefs.some((argRef) => {
34456
- if (isUseStateIdentifier(argRef.identifier)) return false;
34457
- if (isProp(analysis, argRef)) return false;
34458
- if (isUseRefIdentifier(argRef.identifier)) return false;
34459
- if (isRefCurrent(argRef)) return false;
34460
- if (isConstant(argRef)) return false;
34461
- if (isParentWiredHookResultRef(analysis, argRef)) return false;
34462
- if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
34463
- if (resolvesToFunctionBinding(argRef)) return false;
34464
- const argIdentifier = argRef.identifier;
34465
- if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
34466
- if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
34467
- return true;
34468
- })) continue;
34469
- context.report({
34470
- node: callExpr,
34471
- message: "Handing data back to a parent from a useEffect costs your users an extra render."
34472
- });
34473
- }
34474
- } };
34475
- }
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
+ } })
34476
33592
  });
34477
33593
  //#endregion
34478
33594
  //#region src/plugin/utils/is-call-result-consumed-as-argument.ts
@@ -35026,66 +34142,6 @@ const noPropCallbackInEffect = defineRule({
35026
34142
  }
35027
34143
  });
35028
34144
  //#endregion
35029
- //#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
35030
- const isPreservedThroughConciseArrow = (callExpression, scopes) => {
35031
- let node = callExpression;
35032
- let parent = node.parent;
35033
- while (parent) {
35034
- if (isNodeOfType(parent, "ChainExpression")) {
35035
- node = parent;
35036
- parent = node.parent;
35037
- continue;
35038
- }
35039
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
35040
- node = parent;
35041
- parent = node.parent;
35042
- continue;
35043
- }
35044
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
35045
- node = parent;
35046
- parent = node.parent;
35047
- continue;
35048
- }
35049
- if (isNodeOfType(parent, "SequenceExpression")) {
35050
- const expressions = parent.expressions ?? [];
35051
- if (expressions[expressions.length - 1] !== node) return false;
35052
- node = parent;
35053
- parent = node.parent;
35054
- continue;
35055
- }
35056
- if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
35057
- const invocation = parent.parent;
35058
- if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
35059
- if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
35060
- const callee = stripParenExpression(invocation.callee);
35061
- return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
35062
- }
35063
- node = invocation;
35064
- parent = node.parent;
35065
- }
35066
- return false;
35067
- };
35068
- const noPropCallbackInRender = defineRule({
35069
- id: "no-prop-callback-in-render",
35070
- title: "Prop callback invoked during render",
35071
- severity: "error",
35072
- 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.",
35073
- create: (context) => ({ CallExpression(node) {
35074
- if (!isResultDiscardedCall(node)) return;
35075
- if (isPreservedThroughConciseArrow(node, context.scopes)) return;
35076
- if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
35077
- const analysis = getProgramAnalysis(node);
35078
- if (!analysis) return;
35079
- const callee = stripParenExpression(node.callee);
35080
- if (isFunctionLike$1(callee)) return;
35081
- if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
35082
- context.report({
35083
- node,
35084
- 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."
35085
- });
35086
- } })
35087
- });
35088
- //#endregion
35089
34145
  //#region src/plugin/rules/architecture/no-prop-types.ts
35090
34146
  const PROP_TYPES_PROPERTY = "propTypes";
35091
34147
  const isPropTypesKey = (key, computed) => {
@@ -35780,63 +34836,6 @@ const noRedundantShouldComponentUpdate = defineRule({
35780
34836
  }
35781
34837
  });
35782
34838
  //#endregion
35783
- //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
35784
- const resolveReactRefSymbol = (memberExpression, scopes) => {
35785
- const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
35786
- if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
35787
- const symbol = resolveConstIdentifierAlias(receiver, scopes);
35788
- if (!symbol?.initializer) return null;
35789
- const initializer = stripParenExpression(symbol.initializer);
35790
- if (!isNodeOfType(initializer, "CallExpression")) return null;
35791
- return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
35792
- };
35793
- const isSameRefCurrentMember = (node, refSymbol, scopes) => {
35794
- if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
35795
- const receiver = stripParenExpression(node.object);
35796
- return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
35797
- };
35798
- const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
35799
- if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
35800
- let descendant = assignmentExpression;
35801
- let ancestor = descendant.parent;
35802
- while (ancestor) {
35803
- if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
35804
- const { left, right } = ancestor.test;
35805
- if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
35806
- }
35807
- descendant = ancestor;
35808
- ancestor = descendant.parent;
35809
- }
35810
- return false;
35811
- };
35812
- const noRefCurrentInRender = defineRule({
35813
- id: "no-ref-current-in-render",
35814
- title: "Ref mutated during render",
35815
- severity: "error",
35816
- 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.",
35817
- create: (context) => {
35818
- const report = (memberExpression) => {
35819
- if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
35820
- if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
35821
- context.report({
35822
- node: memberExpression,
35823
- message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
35824
- });
35825
- };
35826
- return {
35827
- AssignmentExpression(node) {
35828
- const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
35829
- if (!refSymbol) return;
35830
- if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
35831
- report(node.left);
35832
- },
35833
- UpdateExpression(node) {
35834
- report(node.argument);
35835
- }
35836
- };
35837
- }
35838
- });
35839
- //#endregion
35840
34839
  //#region src/plugin/rules/architecture/no-render-in-render.ts
35841
34840
  const isInsideComponentContext = (node) => {
35842
34841
  let cursor = node.parent;
@@ -56799,18 +55798,6 @@ const reactDoctorRules = [
56799
55798
  requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
56800
55799
  }
56801
55800
  },
56802
- {
56803
- key: "react-doctor/no-impure-state-updater",
56804
- id: "no-impure-state-updater",
56805
- source: "react-doctor",
56806
- originallyExternal: false,
56807
- rule: {
56808
- ...noImpureStateUpdater,
56809
- framework: "global",
56810
- category: "Bugs",
56811
- requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
56812
- }
56813
- },
56814
55801
  {
56815
55802
  key: "react-doctor/no-indeterminate-attribute",
56816
55803
  id: "no-indeterminate-attribute",
@@ -57227,18 +56214,6 @@ const reactDoctorRules = [
57227
56214
  requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
57228
56215
  }
57229
56216
  },
57230
- {
57231
- key: "react-doctor/no-prop-callback-in-render",
57232
- id: "no-prop-callback-in-render",
57233
- source: "react-doctor",
57234
- originallyExternal: false,
57235
- rule: {
57236
- ...noPropCallbackInRender,
57237
- framework: "global",
57238
- category: "Bugs",
57239
- requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
57240
- }
57241
- },
57242
56217
  {
57243
56218
  key: "react-doctor/no-prop-types",
57244
56219
  id: "no-prop-types",
@@ -57330,18 +56305,6 @@ const reactDoctorRules = [
57330
56305
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
57331
56306
  }
57332
56307
  },
57333
- {
57334
- key: "react-doctor/no-ref-current-in-render",
57335
- id: "no-ref-current-in-render",
57336
- source: "react-doctor",
57337
- originallyExternal: false,
57338
- rule: {
57339
- ...noRefCurrentInRender,
57340
- framework: "global",
57341
- category: "Bugs",
57342
- requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
57343
- }
57344
- },
57345
56308
  {
57346
56309
  key: "react-doctor/no-render-in-render",
57347
56310
  id: "no-render-in-render",
@@ -59934,11 +58897,6 @@ const CROSS_FILE_RULE_IDS = new Set([
59934
58897
  "no-indeterminate-attribute",
59935
58898
  "no-locale-format-in-render",
59936
58899
  "no-match-media-in-state-initializer",
59937
- "no-adjust-state-on-prop-change",
59938
- "no-derived-state",
59939
- "no-derived-state-effect",
59940
- "no-event-handler",
59941
- "no-initialize-state",
59942
58900
  "no-mutating-reducer-state",
59943
58901
  "no-unguarded-browser-global-in-render-or-hook-init",
59944
58902
  "prefer-dynamic-import",
@@ -59992,9 +58950,6 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
59992
58950
  if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
59993
58951
  for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
59994
58952
  };
59995
- const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
59996
- for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
59997
- };
59998
58953
  const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
59999
58954
  const namedUseReducerLocals = /* @__PURE__ */ new Set();
60000
58955
  const reactObjectLocals = /* @__PURE__ */ new Set();
@@ -60081,11 +59036,6 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
60081
59036
  ["no-indeterminate-attribute", collectNearestManifestDependencies],
60082
59037
  ["no-locale-format-in-render", collectNearestManifestDependencies],
60083
59038
  ["no-match-media-in-state-initializer", collectNearestManifestDependencies],
60084
- ["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
60085
- ["no-derived-state", collectEffectValueHelperDependencies],
60086
- ["no-derived-state-effect", collectEffectValueHelperDependencies],
60087
- ["no-event-handler", collectEffectValueHelperDependencies],
60088
- ["no-initialize-state", collectEffectValueHelperDependencies],
60089
59039
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
60090
59040
  ["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
60091
59041
  ["prefer-dynamic-import", collectNearestManifestDependencies],