oxlint-plugin-react-doctor 0.7.9-dev.bd3655c → 0.7.9-dev.c245b9d

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.
package/dist/index.d.ts CHANGED
@@ -5503,6 +5503,29 @@ declare const REACT_DOCTOR_RULES: readonly [{
5503
5503
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5504
5504
  readonly create: (context: RuleContext) => RuleVisitors;
5505
5505
  };
5506
+ }, {
5507
+ readonly key: "react-doctor/no-ref-callback-cleanup-before-react-19";
5508
+ readonly id: "no-ref-callback-cleanup-before-react-19";
5509
+ readonly source: "react-doctor";
5510
+ readonly originallyExternal: false;
5511
+ readonly rule: {
5512
+ readonly framework: "global";
5513
+ readonly category: "Bugs";
5514
+ readonly id: string;
5515
+ readonly title?: string;
5516
+ readonly severity: RuleSeverity;
5517
+ readonly requires?: ReadonlyArray<Capability>;
5518
+ readonly disabledWhen?: ReadonlyArray<Capability>;
5519
+ readonly tags?: ReadonlyArray<string>;
5520
+ readonly matchByOccurrence?: boolean;
5521
+ readonly defaultEnabled?: boolean;
5522
+ readonly lifecycle?: "retired";
5523
+ readonly scan?: FileScan;
5524
+ readonly committedFilesOnly?: boolean;
5525
+ readonly recommendation?: string;
5526
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
5527
+ readonly create: (context: RuleContext) => RuleVisitors;
5528
+ };
5506
5529
  }, {
5507
5530
  readonly key: "react-doctor/no-ref-current-in-render";
5508
5531
  readonly id: "no-ref-current-in-render";
@@ -14792,6 +14815,29 @@ declare const RULES: readonly [{
14792
14815
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14793
14816
  readonly create: (context: RuleContext) => RuleVisitors;
14794
14817
  };
14818
+ }, {
14819
+ readonly key: "react-doctor/no-ref-callback-cleanup-before-react-19";
14820
+ readonly id: "no-ref-callback-cleanup-before-react-19";
14821
+ readonly source: "react-doctor";
14822
+ readonly originallyExternal: false;
14823
+ readonly rule: {
14824
+ readonly framework: "global";
14825
+ readonly category: "Bugs";
14826
+ readonly id: string;
14827
+ readonly title?: string;
14828
+ readonly severity: RuleSeverity;
14829
+ readonly requires?: ReadonlyArray<Capability>;
14830
+ readonly disabledWhen?: ReadonlyArray<Capability>;
14831
+ readonly tags?: ReadonlyArray<string>;
14832
+ readonly matchByOccurrence?: boolean;
14833
+ readonly defaultEnabled?: boolean;
14834
+ readonly lifecycle?: "retired";
14835
+ readonly scan?: FileScan;
14836
+ readonly committedFilesOnly?: boolean;
14837
+ readonly recommendation?: string;
14838
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14839
+ readonly create: (context: RuleContext) => RuleVisitors;
14840
+ };
14795
14841
  }, {
14796
14842
  readonly key: "react-doctor/no-ref-current-in-render";
14797
14843
  readonly id: "no-ref-current-in-render";
package/dist/index.js CHANGED
@@ -13652,7 +13652,7 @@ const getPromiseChainCallForCallback = (candidate) => {
13652
13652
  if (!callbackContainer.arguments?.some((argument) => stripParenExpression(argument) === candidate)) return null;
13653
13653
  return isPromiseChainCall(stripParenExpression(callbackContainer.callee)) ? callbackContainer : null;
13654
13654
  };
13655
- const collectEffectInvokedFunctions = (effectCallback) => {
13655
+ const collectInvokedFunctions = (effectCallback, includePromiseCallbacks) => {
13656
13656
  const invokedFunctions = new Set([effectCallback]);
13657
13657
  const localFunctionBindings = /* @__PURE__ */ new Map();
13658
13658
  const calledBindingNames = /* @__PURE__ */ new Set();
@@ -13686,12 +13686,14 @@ const collectEffectInvokedFunctions = (effectCallback) => {
13686
13686
  calledBindingNames.add(callee.name);
13687
13687
  return;
13688
13688
  }
13689
- if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
13689
+ if (includePromiseCallbacks && isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
13690
13690
  });
13691
13691
  for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
13692
13692
  }
13693
13693
  return invokedFunctions;
13694
13694
  };
13695
+ const collectEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, true);
13696
+ const collectSynchronouslyEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, false);
13695
13697
  //#endregion
13696
13698
  //#region src/plugin/utils/is-react-hook-name.ts
13697
13699
  const isReactHookName = (name) => {
@@ -14412,7 +14414,7 @@ const findSingleDirectInvocation = (functionNode, caller, context) => {
14412
14414
  const callNode = findDirectCallForReference(reference.identifier);
14413
14415
  return callNode ? [callNode] : [];
14414
14416
  });
14415
- if (invocationCalls.length !== 1) return null;
14417
+ if (invocationCalls.length !== 1 || symbol.references.length !== 1) return null;
14416
14418
  const invocationCall = invocationCalls[0];
14417
14419
  return findEnclosingFunction$1(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
14418
14420
  };
@@ -14699,7 +14701,108 @@ const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, con
14699
14701
  });
14700
14702
  return hasPotentialInterruption;
14701
14703
  };
14704
+ const getNumericReactRefCurrentKey = (expression, context) => {
14705
+ const refSymbol = resolveReactRefSymbol(stripParenExpression(expression), context.scopes);
14706
+ const initializer = refSymbol?.initializer ? stripParenExpression(refSymbol.initializer) : null;
14707
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
14708
+ const initialValue = initializer.arguments?.[0] ? stripParenExpression(initializer.arguments[0]) : null;
14709
+ if (!isNodeOfType(initialValue, "Literal") || typeof initialValue.value !== "number") return null;
14710
+ return resolveExpressionKey(expression, context);
14711
+ };
14712
+ const getBlockingGenerationKey = (expression, context) => {
14713
+ const test = stripParenExpression(expression);
14714
+ if (isNodeOfType(test, "LogicalExpression") && test.operator === "||") return getBlockingGenerationKey(test.left, context) ?? getBlockingGenerationKey(test.right, context);
14715
+ if (!isNodeOfType(test, "BinaryExpression") || test.operator !== "!==" && test.operator !== "!=") return null;
14716
+ const leftKey = getNumericReactRefCurrentKey(test.left, context);
14717
+ const rightKey = getNumericReactRefCurrentKey(test.right, context);
14718
+ const snapshotExpression = leftKey ? stripParenExpression(test.right) : stripParenExpression(test.left);
14719
+ const key = leftKey ?? rightKey;
14720
+ return key && isNodeOfType(snapshotExpression, "Identifier") ? key : null;
14721
+ };
14722
+ const findGenerationGuardKeyForDeferredUsage = (usageFunction, usageNode, context) => {
14723
+ if (!isFunctionLike$1(usageFunction)) return null;
14724
+ let generationKey = null;
14725
+ walkAst(usageFunction.body, (child) => {
14726
+ if (generationKey) return false;
14727
+ if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
14728
+ if (!isNodeOfType(child, "IfStatement") || child.alternate) return;
14729
+ const key = getBlockingGenerationKey(child.test, context);
14730
+ if (!key || canNodeReachLaterNodeWithinFunction(child.consequent, usageNode, usageFunction, context) || !doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], usageFunction, context)) return;
14731
+ generationKey = key;
14732
+ });
14733
+ return generationKey;
14734
+ };
14735
+ const isGenerationAdvance = (node, generationKey, context) => {
14736
+ if (isNodeOfType(node, "UpdateExpression") && resolveExpressionKey(node.argument, context) === generationKey) return true;
14737
+ if (!isNodeOfType(node, "AssignmentExpression") || resolveExpressionKey(node.left, context) !== generationKey || node.operator !== "+=" && node.operator !== "-=") return false;
14738
+ const amount = stripParenExpression(node.right);
14739
+ return isNodeOfType(amount, "Literal") && typeof amount.value === "number" && amount.value !== 0;
14740
+ };
14741
+ const functionAdvancesGeneration = (owner, generationKey, context) => {
14742
+ if (!isFunctionLike$1(owner)) return false;
14743
+ let didAdvanceGeneration = false;
14744
+ walkAst(owner.body, (child) => {
14745
+ if (didAdvanceGeneration) return false;
14746
+ if (child !== owner.body && isFunctionLike$1(child)) return false;
14747
+ if (isGenerationAdvance(child, generationKey, context)) {
14748
+ didAdvanceGeneration = true;
14749
+ return false;
14750
+ }
14751
+ });
14752
+ return didAdvanceGeneration;
14753
+ };
14754
+ const cleanupReturnsReleaseUsage = (cleanupReturns, usage, context) => cleanupReturns.length > 0 && cleanupReturns.every((cleanupReturn) => {
14755
+ if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
14756
+ const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
14757
+ return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
14758
+ });
14759
+ const getOwnedFunctionReference = (reference, usageFunction, usageNode, callback, cleanupReturns, context) => {
14760
+ const directCall = findDirectCallForReference(reference);
14761
+ if (directCall) {
14762
+ const referenceOwner = findEnclosingFunction$1(directCall);
14763
+ if (referenceOwner && referenceOwner !== usageFunction && collectSynchronouslyEffectInvokedFunctions(callback).has(referenceOwner)) return { generationKey: null };
14764
+ const generationKey = referenceOwner ? findGenerationGuardKeyForDeferredUsage(referenceOwner, directCall, context) : null;
14765
+ return generationKey ? { generationKey } : null;
14766
+ }
14767
+ const referenceRoot = findTransparentExpressionRoot(reference);
14768
+ const schedulerCall = referenceRoot.parent;
14769
+ if (!isNodeOfType(schedulerCall, "CallExpression") || !schedulerCall.arguments.some((argument) => argument === referenceRoot) || !isNodeOfType(schedulerCall.callee, "Identifier") || schedulerCall.callee.name !== "setTimeout" || !context.scopes.isGlobalReference(schedulerCall.callee)) return null;
14770
+ const schedulerUsage = {
14771
+ kind: "timer",
14772
+ node: schedulerCall,
14773
+ resourceName: schedulerCall.callee.name,
14774
+ handleKey: findAssignedResourceKey(schedulerCall, context),
14775
+ receiverKey: null,
14776
+ registrationVerbName: schedulerCall.callee.name,
14777
+ eventKey: null,
14778
+ handlerKey: null
14779
+ };
14780
+ const generationKey = findGenerationGuardKeyForDeferredUsage(usageFunction, usageNode, context);
14781
+ return schedulerUsage.handleKey !== null && cleanupReturnsReleaseUsage(cleanupReturns, schedulerUsage, context) && generationKey ? { generationKey } : null;
14782
+ };
14783
+ const hasGuardedRefOwnedNestedCleanup = (callback, usage, cleanupReturns, context) => {
14784
+ const usageFunction = findEnclosingFunction$1(usage.node);
14785
+ const usageExpression = findTransparentExpressionRoot(usage.node);
14786
+ const usageAssignment = usageExpression.parent;
14787
+ if (usage.kind !== "subscribe" && usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !resolveReactRefSymbol(stripParenExpression(usageAssignment.left), context.scopes) || !collectSynchronouslyEffectInvokedFunctions(callback).has(usageFunction) || !cleanupReturnsReleaseUsage(cleanupReturns, usage, context) || !doMatchingNodesCoverEveryPathFromFunctionEntry(callback, cleanupReturns, context)) return false;
14788
+ const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
14789
+ if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
14790
+ const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
14791
+ return cleanupFunction && isFunctionLike$1(cleanupFunction) ? [cleanupFunction] : [];
14792
+ });
14793
+ const bindingIdentifier = getFunctionBindingIdentifier$1(usageFunction);
14794
+ const functionSymbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
14795
+ if (!functionSymbol || functionSymbol.references.length === 0) return false;
14796
+ const ownedReferences = functionSymbol.references.map((reference) => getOwnedFunctionReference(reference.identifier, usageFunction, usage.node, callback, cleanupReturns, context));
14797
+ if (ownedReferences.some((reference) => reference === null)) return false;
14798
+ const generationKeys = new Set(ownedReferences.flatMap((reference) => reference?.generationKey ? [reference.generationKey] : []));
14799
+ if (generationKeys.size !== 1) return false;
14800
+ const generationKey = generationKeys.values().next().value;
14801
+ if (typeof generationKey !== "string") return false;
14802
+ return [...collectSynchronouslyEffectInvokedFunctions(callback), ...cleanupFunctions].some((owner) => functionAdvancesGeneration(owner, generationKey, context));
14803
+ };
14702
14804
  const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
14805
+ if (hasGuardedRefOwnedNestedCleanup(callback, usage, cleanupReturns, context)) return true;
14703
14806
  const usageFunction = findEnclosingFunction$1(usage.node);
14704
14807
  const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
14705
14808
  if (usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usage.node, "CallExpression") || !isNodeOfType(usage.node.callee, "Identifier") || !context.scopes.isGlobalReference(usage.node.callee) || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
@@ -14911,6 +15014,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
14911
15014
  if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
14912
15015
  if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
14913
15016
  if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
15017
+ if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
14914
15018
  const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
14915
15019
  if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
14916
15020
  const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
@@ -14947,7 +15051,8 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
14947
15051
  const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
14948
15052
  if (!releaseHandler) return releaseVerbName === "off";
14949
15053
  const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
14950
- return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey;
15054
+ const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
15055
+ return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
14951
15056
  }
14952
15057
  if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
14953
15058
  return true;
@@ -14972,13 +15077,188 @@ const isPotentiallyReachableFunction = (functionNode, context) => {
14972
15077
  if (!symbol) return false;
14973
15078
  return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
14974
15079
  };
15080
+ const isJsxRefAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "ref";
15081
+ const isFunctionForwardedToReactRef = (functionNode, context) => {
15082
+ const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
15083
+ if (!bindingIdentifier) return false;
15084
+ const symbol = context.scopes.symbolFor(bindingIdentifier);
15085
+ if (!symbol) return false;
15086
+ return symbol.references.some((reference) => {
15087
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
15088
+ const expressionContainer = referenceRoot.parent;
15089
+ return Boolean(isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === referenceRoot && isJsxRefAttribute(expressionContainer.parent));
15090
+ });
15091
+ };
15092
+ const findRetainedDisposerStorages = (disposerFunction, usage, context) => {
15093
+ if (!isFunctionLike$1(disposerFunction) || disposerFunction.async || disposerFunction.generator) return [];
15094
+ const usageFunction = findEnclosingFunction$1(usage.node);
15095
+ if (!usageFunction || !isFunctionLike$1(usageFunction)) return [];
15096
+ const assignments = /* @__PURE__ */ new Map();
15097
+ const collectAssignment = (expression) => {
15098
+ const expressionRoot = findTransparentExpressionRoot(expression);
15099
+ const assignment = expressionRoot.parent;
15100
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== expressionRoot) return;
15101
+ const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
15102
+ const refCurrentKey = resolveExpressionKey(assignment.left, context);
15103
+ const retainedFunction = findEnclosingFunction$1(assignment);
15104
+ const assignmentStart = getRangeStart(assignment);
15105
+ if (!refSymbol || !refCurrentKey || !retainedFunction || retainedFunction !== usageFunction || assignmentStart === null) return;
15106
+ assignments.set(assignmentStart, {
15107
+ assignmentNode: assignment,
15108
+ refCurrentKey,
15109
+ retainedFunction
15110
+ });
15111
+ };
15112
+ collectAssignment(disposerFunction);
15113
+ const bindingIdentifier = getFunctionBindingIdentifier$1(disposerFunction);
15114
+ const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
15115
+ for (const reference of symbol?.references ?? []) collectAssignment(reference.identifier);
15116
+ walkAst(usageFunction.body, (child) => {
15117
+ if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
15118
+ if (isNodeOfType(child, "AssignmentExpression") && resolveStableValue(child.right, context) === disposerFunction) collectAssignment(child.right);
15119
+ });
15120
+ return [...assignments.values()];
15121
+ };
15122
+ const isRetainedDisposerStorageEstablished = (storage, usage, context) => doMatchingNodesCoverEveryPathBeforeUsage(usage.node, [storage.assignmentNode], storage.retainedFunction, context) || doMatchingNodesCoverEveryPathAfterUsage(usage.node, [storage.assignmentNode], context);
15123
+ const hasUnsafeRetainedDisposerOverwrite = (storage, usage, context) => {
15124
+ let hasUnsafeOverwrite = false;
15125
+ walkAst(storage.retainedFunction.body, (child) => {
15126
+ if (hasUnsafeOverwrite) return false;
15127
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15128
+ if (!isNodeOfType(child, "AssignmentExpression") || child === storage.assignmentNode || resolveExpressionKey(child.left, context) !== storage.refCurrentKey || !canNodeReachLaterNodeWithinFunction(usage.node, child, storage.retainedFunction, context)) return;
15129
+ const storedValue = resolveStableValue(child.right, context);
15130
+ if (!storedValue || !isFunctionLike$1(storedValue) || !doesCleanupFunctionReleaseUsage(storedValue, usage, context)) {
15131
+ hasUnsafeOverwrite = true;
15132
+ return false;
15133
+ }
15134
+ });
15135
+ return hasUnsafeOverwrite;
15136
+ };
15137
+ const hasEffectCleanupInvocation = (storage, usage, context) => {
15138
+ const componentFunction = findEnclosingFunction$1(storage.retainedFunction);
15139
+ if (!componentFunction || !isFunctionLike$1(componentFunction)) return false;
15140
+ const cleanupFunctionInvokesRef = (cleanupFunction) => {
15141
+ if (!isFunctionLike$1(cleanupFunction)) return false;
15142
+ let didFindCleanupCall = false;
15143
+ walkAst(cleanupFunction.body, (child) => {
15144
+ if (didFindCleanupCall) return false;
15145
+ if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
15146
+ if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) {
15147
+ const callRoot = findTransparentExpressionRoot(child);
15148
+ const callStatement = callRoot.parent;
15149
+ const isDirectBlockStatement = isNodeOfType(cleanupFunction.body, "BlockStatement") && isNodeOfType(callStatement, "ExpressionStatement") && callStatement.parent === cleanupFunction.body;
15150
+ const isConciseBody = cleanupFunction.body === callRoot;
15151
+ if ((isDirectBlockStatement || isConciseBody) && !hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, child, storage.refCurrentKey, context)) {
15152
+ didFindCleanupCall = true;
15153
+ return false;
15154
+ }
15155
+ }
15156
+ });
15157
+ return didFindCleanupCall;
15158
+ };
15159
+ const effectReturnsCleanup = (effectCallback) => {
15160
+ if (!isFunctionLike$1(effectCallback)) return false;
15161
+ if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
15162
+ const cleanupFunction = resolveRefOwnedCleanupFunction(effectCallback.body, context);
15163
+ return Boolean(cleanupFunction && cleanupFunctionInvokesRef(cleanupFunction));
15164
+ }
15165
+ const matchingReturns = [];
15166
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
15167
+ if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
15168
+ const cleanupFunction = resolveRefOwnedCleanupFunction(child.argument, context);
15169
+ if (!cleanupFunction || !cleanupFunctionInvokesRef(cleanupFunction)) return;
15170
+ matchingReturns.push(child);
15171
+ });
15172
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
15173
+ };
15174
+ let didFindInvocation = false;
15175
+ walkAst(componentFunction.body, (child) => {
15176
+ if (didFindInvocation) return false;
15177
+ if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
15178
+ const effectCallback = getEffectCallback(child);
15179
+ if (effectCallback && effectReturnsCleanup(effectCallback)) {
15180
+ didFindInvocation = true;
15181
+ return false;
15182
+ }
15183
+ });
15184
+ return didFindInvocation;
15185
+ };
15186
+ const hasCallbackRefReplacementInvocation = (storage, usage, context) => {
15187
+ const isReturnedCallbackRefShape = () => {
15188
+ if (!isFunctionLike$1(storage.retainedFunction)) return false;
15189
+ const callbackCall = findTransparentExpressionRoot(storage.retainedFunction).parent;
15190
+ if (!isNodeOfType(callbackCall, "CallExpression") || !isReactApiCall(callbackCall, "useCallback", context.scopes)) return false;
15191
+ const nodeParameter = storage.retainedFunction.params?.[0];
15192
+ const nodeParameterKey = resolveExpressionKey(nodeParameter, context);
15193
+ if (!nodeParameterKey || usage.receiverKey !== nodeParameterKey) return false;
15194
+ const bindingIdentifier = getFunctionBindingIdentifier$1(storage.retainedFunction);
15195
+ const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
15196
+ if (!Boolean(symbol?.references.some((reference) => {
15197
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
15198
+ const property = referenceRoot.parent;
15199
+ if (!isNodeOfType(property, "Property") || property.value !== referenceRoot || !isNodeOfType(property.parent, "ObjectExpression")) return false;
15200
+ const returnedObject = findTransparentExpressionRoot(property.parent);
15201
+ const returnStatement = returnedObject.parent;
15202
+ if (!isNodeOfType(returnStatement, "ReturnStatement") || returnStatement.argument !== returnedObject) return false;
15203
+ const hookFunction = findEnclosingFunction$1(returnStatement);
15204
+ return Boolean(hookFunction && getFunctionBindingIdentifier$1(hookFunction)?.name.startsWith("use"));
15205
+ }))) return false;
15206
+ const usageStart = getRangeStart(usage.node);
15207
+ if (usageStart === null) return false;
15208
+ let hasNullExit = false;
15209
+ walkAst(storage.retainedFunction.body, (child) => {
15210
+ if (hasNullExit) return false;
15211
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15212
+ if (!isNodeOfType(child, "IfStatement") || (getRangeStart(child) ?? usageStart) >= usageStart) return;
15213
+ const test = stripParenExpression(child.test);
15214
+ if (!isNodeOfType(test, "UnaryExpression") || test.operator !== "!" || resolveExpressionKey(test.argument, context) !== nodeParameterKey) return;
15215
+ const consequent = child.consequent;
15216
+ hasNullExit = isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && consequent.body.some((statement) => isNodeOfType(statement, "ReturnStatement"));
15217
+ if (hasNullExit) return false;
15218
+ });
15219
+ return hasNullExit;
15220
+ };
15221
+ if (!isFunctionForwardedToReactRef(storage.retainedFunction, context) && !isReturnedCallbackRefShape()) return false;
15222
+ const cleanupCalls = [];
15223
+ walkAst(storage.retainedFunction.body, (child) => {
15224
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15225
+ if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) cleanupCalls.push(child);
15226
+ });
15227
+ return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, cleanupCalls, storage.retainedFunction, context);
15228
+ };
15229
+ const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
15230
+ const disposerFunction = findEnclosingFunction$1(releaseNode);
15231
+ if (!disposerFunction) return false;
15232
+ return findRetainedDisposerStorages(disposerFunction, usage, context).some((storage) => isRetainedDisposerStorageEstablished(storage, usage, context) && !hasUnsafeRetainedDisposerOverwrite(storage, usage, context) && (hasEffectCleanupInvocation(storage, usage, context) || hasCallbackRefReplacementInvocation(storage, usage, context)));
15233
+ };
15234
+ const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
15235
+ if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
15236
+ const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
15237
+ const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
15238
+ if (!isNodeOfType(releaseCall, "CallExpression")) return false;
15239
+ const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
15240
+ if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
15241
+ const ownerFunction = findEnclosingFunction$1(releaseFunction);
15242
+ if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
15243
+ const triggerRegistrations = [];
15244
+ walkAst(ownerFunction.body, (child) => {
15245
+ if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
15246
+ if (!isNodeOfType(child, "CallExpression")) return;
15247
+ const registrationDetails = getCallRegistrationDetails(child, context);
15248
+ if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
15249
+ });
15250
+ if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
15251
+ return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
15252
+ };
14975
15253
  const isReleaseReachableForUsage = (releaseNode, usage, context) => {
14976
15254
  if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
14977
15255
  const releaseFunction = findEnclosingFunction$1(releaseNode);
14978
15256
  if (!releaseFunction) return true;
14979
15257
  if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
15258
+ if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
14980
15259
  const usageFunction = findEnclosingFunction$1(usage.node);
14981
15260
  if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
15261
+ if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
14982
15262
  return isPotentiallyReachableFunction(releaseFunction, context);
14983
15263
  };
14984
15264
  const fileContainsReleaseForUsage = (usage, context) => {
@@ -17112,7 +17392,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
17112
17392
  keys.add(depKey);
17113
17393
  continue;
17114
17394
  }
17115
- const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
17395
+ const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
17116
17396
  if (identitySourceKeys) {
17117
17397
  if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
17118
17398
  for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
@@ -17195,6 +17475,161 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
17195
17475
  if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
17196
17476
  return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
17197
17477
  };
17478
+ const isPureDerivedExpression = (expression) => {
17479
+ const candidate = unwrapExpression$3(expression);
17480
+ if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
17481
+ if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
17482
+ if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
17483
+ if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
17484
+ if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
17485
+ if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
17486
+ return false;
17487
+ };
17488
+ const isPureDerivedStatement = (statement) => {
17489
+ if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
17490
+ if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
17491
+ if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
17492
+ return false;
17493
+ };
17494
+ const isPureDerivedFunction = (functionNode) => {
17495
+ if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
17496
+ if (functionNode.async || functionNode.generator) return false;
17497
+ return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
17498
+ };
17499
+ const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
17500
+ if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
17501
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
17502
+ const callExpression = referenceRoot.parent;
17503
+ if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
17504
+ const functionNode = getFunctionValueNode(symbol);
17505
+ if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
17506
+ const sourceKeys = /* @__PURE__ */ new Set();
17507
+ for (const capturedReference of closureCaptures(functionNode, scopes)) {
17508
+ const capturedSymbol = capturedReference.resolvedSymbol;
17509
+ if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
17510
+ if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
17511
+ const capturedKey = computeDepKey(capturedReference);
17512
+ if (!capturedKey) return null;
17513
+ if (capturedKey === capturedSymbol.name) {
17514
+ const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
17515
+ if (nestedSourceKeys) {
17516
+ for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
17517
+ continue;
17518
+ }
17519
+ }
17520
+ sourceKeys.add(capturedKey);
17521
+ }
17522
+ return sourceKeys.size > 0 ? sourceKeys : null;
17523
+ };
17524
+ const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
17525
+ const sourceKeys = /* @__PURE__ */ new Set();
17526
+ for (const expression of expressions) {
17527
+ const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
17528
+ if (!expressionSourceKeys) return null;
17529
+ for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
17530
+ }
17531
+ return sourceKeys;
17532
+ };
17533
+ const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
17534
+ const candidate = unwrapExpression$3(expression);
17535
+ if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
17536
+ if (isNodeOfType(candidate, "Identifier")) {
17537
+ if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
17538
+ const sourceSymbol = scopes.symbolFor(candidate);
17539
+ if (!sourceSymbol) return null;
17540
+ if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
17541
+ if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((sourceReference) => sourceReference.flag === "read") && !visitedSymbolIds.has(sourceSymbol.id)) {
17542
+ visitedSymbolIds.add(sourceSymbol.id);
17543
+ const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
17544
+ visitedSymbolIds.delete(sourceSymbol.id);
17545
+ if (sourceKeys) return sourceKeys;
17546
+ }
17547
+ return new Set([sourceSymbol.name]);
17548
+ }
17549
+ if (isNodeOfType(candidate, "MemberExpression")) {
17550
+ if (hasComputedMemberExpression(candidate)) return null;
17551
+ const sourceKey = stringifyMemberChain(candidate);
17552
+ const rootIdentifier = getMemberRootIdentifier(candidate);
17553
+ const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
17554
+ if (!sourceKey || !rootSymbol) return null;
17555
+ if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
17556
+ return new Set([sourceKey]);
17557
+ }
17558
+ if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
17559
+ if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
17560
+ if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
17561
+ candidate.test,
17562
+ candidate.consequent,
17563
+ candidate.alternate
17564
+ ], scopes, visitedSymbolIds);
17565
+ if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
17566
+ if (isNodeOfType(candidate, "NewExpression")) {
17567
+ const callee = unwrapExpression$3(candidate.callee);
17568
+ if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
17569
+ const argumentsToAnalyze = [];
17570
+ for (const argument of candidate.arguments) {
17571
+ if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
17572
+ argumentsToAnalyze.push(argument);
17573
+ }
17574
+ return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
17575
+ }
17576
+ return null;
17577
+ };
17578
+ const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
17579
+ const sourceKeys = /* @__PURE__ */ new Set();
17580
+ let currentNode = assignment;
17581
+ while (currentNode.parent && currentNode.parent !== boundaryFunction) {
17582
+ const parentNode = currentNode.parent;
17583
+ if (isNodeOfType(parentNode, "IfStatement")) {
17584
+ if (parentNode.test === currentNode) return null;
17585
+ const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
17586
+ if (!testSourceKeys) return null;
17587
+ for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
17588
+ } else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
17589
+ currentNode = parentNode;
17590
+ }
17591
+ return currentNode.parent === boundaryFunction ? sourceKeys : null;
17592
+ };
17593
+ const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
17594
+ const referenceRoot = findTransparentExpressionRoot(referenceNode);
17595
+ const callExpression = referenceRoot.parent;
17596
+ return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
17597
+ allowGlobalReactNamespace: true,
17598
+ allowUnboundBareCalls: true,
17599
+ resolveNamedAliases: true
17600
+ });
17601
+ };
17602
+ const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
17603
+ if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
17604
+ const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
17605
+ if (!boundaryFunction) return null;
17606
+ const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
17607
+ if (!capturingFunction || capturingFunction === boundaryFunction) return null;
17608
+ const sourceKeys = /* @__PURE__ */ new Set();
17609
+ if (symbol.initializer) {
17610
+ const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
17611
+ if (!initializerSourceKeys) return null;
17612
+ for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
17613
+ }
17614
+ let writeCount = 0;
17615
+ for (const symbolReference of symbol.references) {
17616
+ if (symbolReference.flag === "read") {
17617
+ if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
17618
+ continue;
17619
+ }
17620
+ if (symbolReference.flag !== "write") return null;
17621
+ const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
17622
+ const assignment = referenceRoot.parent;
17623
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
17624
+ const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
17625
+ const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
17626
+ if (!assignmentSourceKeys || !controlSourceKeys) return null;
17627
+ for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
17628
+ for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
17629
+ writeCount += 1;
17630
+ }
17631
+ return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
17632
+ };
17198
17633
  const isUseCallbackResultDep = (node, scopes) => {
17199
17634
  const rootSymbol = getRootSymbol(node, scopes);
17200
17635
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
@@ -40263,6 +40698,17 @@ const DOM_MEASUREMENT_NAMES = new Set([
40263
40698
  "scrollHeight"
40264
40699
  ]);
40265
40700
  const MEASUREMENT_HELPER_CALLEE_PATTERN = /^(?:get|measure|read)\w*(?:Width|Height|Rect|Rects|Size|Bounds|Position)$/;
40701
+ const IMPERATIVE_DOM_MUTATION_NAMES = new Set([
40702
+ "blur",
40703
+ "focus",
40704
+ "restoreSelection",
40705
+ "scroll",
40706
+ "scrollBy",
40707
+ "scrollIntoView",
40708
+ "scrollTo",
40709
+ "setRangeText",
40710
+ "setSelectionRange"
40711
+ ]);
40266
40712
  const subtreeReadsDomMeasurement = (root) => {
40267
40713
  if (!root) return false;
40268
40714
  let found = false;
@@ -40281,29 +40727,66 @@ const subtreeReadsDomMeasurement = (root) => {
40281
40727
  });
40282
40728
  return found;
40283
40729
  };
40284
- const collectMeasuringFunctionNames = (program) => {
40730
+ const collectFunctionNamesMatchingBody = (program, matchesBody) => {
40285
40731
  const names = /* @__PURE__ */ new Set();
40286
40732
  walkAst(program, (child) => {
40287
40733
  if (isNodeOfType(child, "FunctionDeclaration")) {
40288
- if (child.id && isNodeOfType(child.id, "Identifier") && subtreeReadsDomMeasurement(child.body)) names.add(child.id.name);
40734
+ if (child.id && isNodeOfType(child.id, "Identifier") && matchesBody(child.body)) names.add(child.id.name);
40289
40735
  return;
40290
40736
  }
40291
40737
  if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier")) return;
40292
40738
  let functionValue = child.init;
40293
40739
  if (functionValue && isNodeOfType(functionValue, "CallExpression") && isNodeOfType(functionValue.callee, "Identifier") && /^use[A-Z]/.test(functionValue.callee.name)) functionValue = functionValue.arguments?.[0];
40294
- if (functionValue && isFunctionLike$1(functionValue) && subtreeReadsDomMeasurement(functionValue.body)) names.add(child.id.name);
40740
+ if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
40295
40741
  });
40296
40742
  return names;
40297
40743
  };
40298
- const callsAnyName = (root, names) => {
40299
- if (!root || names.size === 0) return false;
40744
+ const collectMeasuringFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeReadsDomMeasurement);
40745
+ const subtreeMutatesDomImperatively = (root) => {
40746
+ if (!root || isFunctionLike$1(root)) return false;
40747
+ let found = false;
40748
+ walkAst(root, (child) => {
40749
+ if (found) return false;
40750
+ if (child !== root && isFunctionLike$1(child)) return false;
40751
+ if (!isNodeOfType(child, "CallExpression")) return;
40752
+ const callee = stripParenExpression(child.callee);
40753
+ const propertyName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
40754
+ if (propertyName !== null && IMPERATIVE_DOM_MUTATION_NAMES.has(propertyName)) {
40755
+ found = true;
40756
+ return false;
40757
+ }
40758
+ });
40759
+ return found;
40760
+ };
40761
+ const collectImperativeDomFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeMutatesDomImperatively);
40762
+ const callsAnyName = (root, names, shouldSkipNestedFunctions = false) => {
40763
+ if (!root || names.size === 0 || shouldSkipNestedFunctions && isFunctionLike$1(root)) return false;
40300
40764
  let found = false;
40301
40765
  walkAst(root, (child) => {
40302
40766
  if (found) return false;
40767
+ if (shouldSkipNestedFunctions && child !== root && isFunctionLike$1(child)) return false;
40303
40768
  if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && names.has(child.callee.name)) found = true;
40304
40769
  });
40305
40770
  return found;
40306
40771
  };
40772
+ const isFollowedByImperativeDomMutation = (call, imperativeDomFunctionNames) => {
40773
+ let statement = call;
40774
+ let parent = statement.parent;
40775
+ while (parent) {
40776
+ const statements = isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program") || isNodeOfType(parent, "StaticBlock") ? parent.body : isNodeOfType(parent, "SwitchCase") ? parent.consequent : null;
40777
+ if (statements) {
40778
+ const statementIndex = statements.findIndex((siblingStatement) => siblingStatement === statement);
40779
+ if (statementIndex >= 0) {
40780
+ const nextStatement = statements[statementIndex + 1];
40781
+ return subtreeMutatesDomImperatively(nextStatement) || callsAnyName(nextStatement, imperativeDomFunctionNames, true);
40782
+ }
40783
+ }
40784
+ if (isFunctionLike$1(parent) || parent.type.endsWith("Statement") && !isNodeOfType(parent, "ExpressionStatement")) return false;
40785
+ statement = parent;
40786
+ parent = parent.parent;
40787
+ }
40788
+ return false;
40789
+ };
40307
40790
  const isInsideStartViewTransition = (node) => {
40308
40791
  let cursor = node.parent;
40309
40792
  while (cursor) {
@@ -40344,11 +40827,12 @@ const importsImperativeDomLibrary = (program) => {
40344
40827
  };
40345
40828
  const hasExemptFlushSyncCall = (program, localName) => {
40346
40829
  const measuringFunctionNames = collectMeasuringFunctionNames(program);
40830
+ const imperativeDomFunctionNames = collectImperativeDomFunctionNames(program);
40347
40831
  let exempt = false;
40348
40832
  walkAst(program, (child) => {
40349
40833
  if (exempt) return false;
40350
40834
  if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "Identifier") || child.callee.name !== localName) return;
40351
- if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames)) {
40835
+ if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames) || isFollowedByImperativeDomMutation(child, imperativeDomFunctionNames)) {
40352
40836
  exempt = true;
40353
40837
  return false;
40354
40838
  }
@@ -40913,41 +41397,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
40913
41397
  if (leftResult === false && rightResult === false) return false;
40914
41398
  return null;
40915
41399
  };
40916
- const readHydrationConditionResult = (expression, context, runtime) => {
41400
+ const readHydrationConditionResult = (expression, context, runtime, state) => {
40917
41401
  const unwrappedExpression = stripParenExpression(expression);
40918
41402
  const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
40919
41403
  if (predicateMatch) return predicateMatch[`${runtime}Result`];
40920
41404
  const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
40921
41405
  if (staticResult !== null) return staticResult;
41406
+ const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
41407
+ const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
41408
+ if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
41409
+ state.visitedSymbolIds.add(expressionSymbol.id);
41410
+ const result = readHydrationConditionResult(parameterValue, context, runtime, state);
41411
+ state.visitedSymbolIds.delete(expressionSymbol.id);
41412
+ return result;
41413
+ }
41414
+ if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
41415
+ state.visitedSymbolIds.add(expressionSymbol.id);
41416
+ const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
41417
+ state.visitedSymbolIds.delete(expressionSymbol.id);
41418
+ return result;
41419
+ }
41420
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
41421
+ const callArguments = unwrappedExpression.arguments ?? [];
41422
+ if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
41423
+ allowGlobalReactNamespace: true,
41424
+ resolveNamedAliases: true
41425
+ })) {
41426
+ const callbackArgument = callArguments[0];
41427
+ if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
41428
+ const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
41429
+ return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
41430
+ }
41431
+ const callee = stripParenExpression(unwrappedExpression.callee);
41432
+ if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return readHydrationConditionResult(callArguments[0], context, runtime, state);
41433
+ const helperFunction = resolveExactLocalFunction(callee, context.scopes);
41434
+ if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
41435
+ const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
41436
+ for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
41437
+ const parameter = helperFunction.params[parameterIndex];
41438
+ const argument = callArguments[parameterIndex];
41439
+ if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
41440
+ const parameterSymbol = context.scopes.symbolFor(parameter);
41441
+ if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
41442
+ }
41443
+ return readHydrationFunctionResult(helperFunction, context, runtime, {
41444
+ ...state,
41445
+ parameterValuesBySymbolId
41446
+ });
41447
+ }
40922
41448
  if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
40923
- const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
41449
+ const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
40924
41450
  return argumentResult === null ? null : !argumentResult;
40925
41451
  }
40926
41452
  if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
40927
- return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
41453
+ return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
41454
+ };
41455
+ const readHydrationStatementResult = (statement, context, runtime, state) => {
41456
+ if (isNodeOfType(statement, "ReturnStatement")) return {
41457
+ didReturn: true,
41458
+ value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
41459
+ };
41460
+ if (isNodeOfType(statement, "BlockStatement")) {
41461
+ for (const childStatement of statement.body) {
41462
+ const result = readHydrationStatementResult(childStatement, context, runtime, state);
41463
+ if (result.didReturn) return result;
41464
+ if (statementAlwaysExits(childStatement)) break;
41465
+ }
41466
+ return {
41467
+ didReturn: false,
41468
+ value: null
41469
+ };
41470
+ }
41471
+ if (!isNodeOfType(statement, "IfStatement")) return {
41472
+ didReturn: false,
41473
+ value: null
41474
+ };
41475
+ const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
41476
+ if (conditionResult !== null) {
41477
+ const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
41478
+ return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
41479
+ didReturn: false,
41480
+ value: null
41481
+ };
41482
+ }
41483
+ const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
41484
+ const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
41485
+ didReturn: false,
41486
+ value: null
41487
+ };
41488
+ return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
41489
+ didReturn: consequentResult.didReturn || alternateResult.didReturn,
41490
+ value: null
41491
+ };
41492
+ };
41493
+ const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
41494
+ if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
41495
+ state.visitedFunctionNodes.add(functionNode);
41496
+ const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
41497
+ state.visitedFunctionNodes.delete(functionNode);
41498
+ return result;
41499
+ };
41500
+ const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
41501
+ const left = stripParenExpression(leftExpression);
41502
+ const right = stripParenExpression(rightExpression);
41503
+ if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
41504
+ const leftSymbol = scopes.symbolFor(left);
41505
+ const rightSymbol = scopes.symbolFor(right);
41506
+ return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
41507
+ }
41508
+ if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
41509
+ if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
41510
+ const rightArguments = right.arguments ?? [];
41511
+ return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
41512
+ const rightArgument = rightArguments[index];
41513
+ return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
41514
+ });
41515
+ }
41516
+ return true;
41517
+ };
41518
+ const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
41519
+ if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
41520
+ const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
41521
+ const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
41522
+ return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
41523
+ };
41524
+ const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
41525
+ const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
41526
+ return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
40928
41527
  };
40929
- const matchHydrationCondition = (expression, context) => {
41528
+ const matchHydrationConditionInternal = (expression, context, state) => {
40930
41529
  const unwrappedExpression = stripParenExpression(expression);
40931
41530
  const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
40932
41531
  if (predicateMatch) return {
40933
41532
  predicateMatch,
40934
41533
  predicateNode: unwrappedExpression
40935
41534
  };
40936
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
40937
- if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
40938
- const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
40939
- const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
40940
- if (leftMatch && rightMatch) {
40941
- const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
40942
- const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
40943
- return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
41535
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
41536
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
41537
+ const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
41538
+ if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
41539
+ state.visitedSymbolIds.add(symbol.id);
41540
+ const match = matchHydrationConditionInternal(parameterValue, context, state);
41541
+ state.visitedSymbolIds.delete(symbol.id);
41542
+ return match;
41543
+ }
41544
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
41545
+ state.visitedSymbolIds.add(symbol.id);
41546
+ const match = matchHydrationConditionInternal(symbol.initializer, context, state);
41547
+ state.visitedSymbolIds.delete(symbol.id);
41548
+ return match;
41549
+ }
41550
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
41551
+ const callArguments = unwrappedExpression.arguments ?? [];
41552
+ if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
41553
+ allowGlobalReactNamespace: true,
41554
+ resolveNamedAliases: true
41555
+ })) {
41556
+ const callbackArgument = callArguments[0];
41557
+ if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
41558
+ const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
41559
+ return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
41560
+ }
41561
+ const callee = stripParenExpression(unwrappedExpression.callee);
41562
+ if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
41563
+ const helperFunction = resolveExactLocalFunction(callee, context.scopes);
41564
+ if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
41565
+ const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
41566
+ for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
41567
+ const parameter = helperFunction.params[parameterIndex];
41568
+ const argument = callArguments[parameterIndex];
41569
+ if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
41570
+ const parameterSymbol = context.scopes.symbolFor(parameter);
41571
+ if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
41572
+ }
41573
+ return matchHydrationFunctionResult(helperFunction, context, {
41574
+ ...state,
41575
+ parameterValuesBySymbolId
41576
+ });
40944
41577
  }
41578
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
41579
+ if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
41580
+ const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
41581
+ const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
40945
41582
  const nestedMatch = leftMatch ?? rightMatch;
40946
41583
  if (!nestedMatch) return null;
40947
- const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
40948
- if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
40949
- return nestedMatch;
41584
+ const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
41585
+ const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
41586
+ return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
40950
41587
  };
41588
+ const matchHydrationReturningStatement = (statement, context, state) => {
41589
+ if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
41590
+ if (isNodeOfType(statement, "IfStatement")) {
41591
+ const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
41592
+ const consequentValues = getReturnedValues(statement.consequent);
41593
+ const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
41594
+ if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
41595
+ return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
41596
+ }
41597
+ if (!isNodeOfType(statement, "BlockStatement")) return null;
41598
+ for (const childStatement of statement.body) {
41599
+ const match = matchHydrationReturningStatement(childStatement, context, state);
41600
+ if (match) return match;
41601
+ if (statementAlwaysExits(childStatement)) break;
41602
+ }
41603
+ return null;
41604
+ };
41605
+ const matchHydrationFunctionResult = (functionNode, context, state) => {
41606
+ if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
41607
+ state.visitedFunctionNodes.add(functionNode);
41608
+ const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
41609
+ state.visitedFunctionNodes.delete(functionNode);
41610
+ return match;
41611
+ };
41612
+ const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
41613
+ parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
41614
+ visitedFunctionNodes: /* @__PURE__ */ new Set(),
41615
+ visitedSymbolIds: /* @__PURE__ */ new Set()
41616
+ });
40951
41617
  const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
40952
41618
  const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
40953
41619
  if (!leftNode || !rightNode) return leftNode === rightNode;
@@ -41090,17 +41756,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
41090
41756
  const { predicateMatch, predicateNode } = conditionMatch;
41091
41757
  if (reportedNodes.has(predicateNode)) return;
41092
41758
  if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
41093
- const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
41759
+ const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
41094
41760
  if (!componentOrHookNode) return;
41095
41761
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
41096
- if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
41762
+ if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
41097
41763
  if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
41098
- const attribute = findEnclosingJsxAttribute(predicateNode);
41764
+ const attribute = findEnclosingJsxAttribute(conditionNode);
41099
41765
  if (!attribute || isEventHandlerAttribute(attribute)) return;
41100
41766
  }
41101
- if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
41102
- if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
41103
- const openingElement = findEnclosingJsxOpeningElement(predicateNode);
41767
+ if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
41768
+ if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
41769
+ const openingElement = findEnclosingJsxOpeningElement(conditionNode);
41104
41770
  if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
41105
41771
  if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
41106
41772
  if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
@@ -41840,7 +42506,8 @@ const noJsxElementType = defineRule({
41840
42506
  create: (context) => {
41841
42507
  let isJsxImported = false;
41842
42508
  const flaggedAnnotations = [];
41843
- const checkReturnType = (returnType) => {
42509
+ const collectComponentReturnType = (functionNode, returnType) => {
42510
+ if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
41844
42511
  const typeAnnotation = extractReturnTypeAnnotation(returnType);
41845
42512
  if (!typeAnnotation) return;
41846
42513
  if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
@@ -41850,19 +42517,16 @@ const noJsxElementType = defineRule({
41850
42517
  if (isJsxImportBinding(node)) isJsxImported = true;
41851
42518
  },
41852
42519
  FunctionDeclaration(node) {
41853
- checkReturnType(node.returnType);
42520
+ collectComponentReturnType(node, node.returnType);
41854
42521
  },
41855
42522
  ArrowFunctionExpression(node) {
41856
- checkReturnType(node.returnType);
42523
+ collectComponentReturnType(node, node.returnType);
41857
42524
  },
41858
42525
  FunctionExpression(node) {
41859
- checkReturnType(node.returnType);
42526
+ collectComponentReturnType(node, node.returnType);
41860
42527
  },
41861
42528
  TSDeclareFunction(node) {
41862
- checkReturnType(node.returnType);
41863
- },
41864
- TSMethodSignature(node) {
41865
- checkReturnType(node.returnType);
42529
+ collectComponentReturnType(node, node.returnType);
41866
42530
  },
41867
42531
  "Program:exit"() {
41868
42532
  if (isJsxImported) return;
@@ -47086,6 +47750,69 @@ const noRedundantShouldComponentUpdate = defineRule({
47086
47750
  }
47087
47751
  });
47088
47752
  //#endregion
47753
+ //#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
47754
+ const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
47755
+ const expression = stripParenExpression(rawExpression);
47756
+ if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
47757
+ if (isNodeOfType(expression, "ConditionalExpression")) {
47758
+ if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
47759
+ return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
47760
+ }
47761
+ if (isNodeOfType(expression, "LogicalExpression")) {
47762
+ if (isNodeOfType(expression.left, "Literal")) {
47763
+ const isLeftTruthy = Boolean(expression.left.value);
47764
+ if (expression.operator === "&&" && !isLeftTruthy) return [];
47765
+ if (expression.operator === "||" && isLeftTruthy) return [];
47766
+ if (expression.operator === "??" && expression.left.value !== null) return [];
47767
+ }
47768
+ if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
47769
+ return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
47770
+ }
47771
+ if (isNodeOfType(expression, "SequenceExpression")) {
47772
+ const finalExpression = expression.expressions.at(-1);
47773
+ return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
47774
+ }
47775
+ if (isNodeOfType(expression, "CallExpression")) {
47776
+ if (!isReactApiCall(expression, "useCallback", scopes)) return [];
47777
+ const callback = expression.arguments[0];
47778
+ return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
47779
+ }
47780
+ if (!isNodeOfType(expression, "Identifier")) return [];
47781
+ const symbol = scopes.symbolFor(expression);
47782
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
47783
+ if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration") && symbol.references.every((reference) => reference.flag === "read")) return resolveFunctionExpressions(symbol.declarationNode, scopes, new Set([...visitedSymbolIds, symbol.id]));
47784
+ const initializer = getDirectConstInitializer(symbol);
47785
+ if (!initializer) return [];
47786
+ return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
47787
+ };
47788
+ const functionReturnsCleanupFunction = (functionExpression, scopes) => {
47789
+ if (!isFunctionLike$1(functionExpression)) return false;
47790
+ if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
47791
+ return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
47792
+ };
47793
+ const callbackReturnsCleanupFunction = (callback, scopes) => {
47794
+ return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
47795
+ };
47796
+ const noRefCallbackCleanupBeforeReact19 = defineRule({
47797
+ id: "no-ref-callback-cleanup-before-react-19",
47798
+ title: "Ref cleanup requires React 19",
47799
+ requires: ["react:18"],
47800
+ disabledWhen: ["react:19"],
47801
+ severity: "warn",
47802
+ recommendation: "React 18 ignores functions returned from ref callbacks. Handle cleanup when React calls the ref with `null`, or require React 19 before returning a cleanup function.",
47803
+ create: (context) => ({ JSXAttribute(node) {
47804
+ if (getJsxAttributeName(node.name) !== "ref") return;
47805
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
47806
+ const callback = node.value.expression;
47807
+ if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
47808
+ if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
47809
+ context.report({
47810
+ node,
47811
+ message: "This ref callback returns a cleanup function, but React 18 ignores ref cleanup returns, so the cleanup never runs. Handle detachment when React calls the ref with `null`, or require React 19."
47812
+ });
47813
+ } })
47814
+ });
47815
+ //#endregion
47089
47816
  //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
47090
47817
  const REPEATED_ANCESTOR_TYPES = new Set([
47091
47818
  "DoWhileStatement",
@@ -71853,6 +72580,17 @@ const reactDoctorRules = [
71853
72580
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
71854
72581
  }
71855
72582
  },
72583
+ {
72584
+ key: "react-doctor/no-ref-callback-cleanup-before-react-19",
72585
+ id: "no-ref-callback-cleanup-before-react-19",
72586
+ source: "react-doctor",
72587
+ originallyExternal: false,
72588
+ rule: {
72589
+ ...noRefCallbackCleanupBeforeReact19,
72590
+ framework: "global",
72591
+ category: "Bugs"
72592
+ }
72593
+ },
71856
72594
  {
71857
72595
  key: "react-doctor/no-ref-current-in-render",
71858
72596
  id: "no-ref-current-in-render",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.9-dev.bd3655c",
3
+ "version": "0.7.9-dev.c245b9d",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",