oxlint-plugin-react-doctor 0.7.9-dev.093619c → 0.7.9-dev.0ff57fa

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,29 +5503,6 @@ 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
- };
5529
5506
  }, {
5530
5507
  readonly key: "react-doctor/no-ref-current-in-render";
5531
5508
  readonly id: "no-ref-current-in-render";
@@ -14815,29 +14792,6 @@ declare const RULES: readonly [{
14815
14792
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
14816
14793
  readonly create: (context: RuleContext) => RuleVisitors;
14817
14794
  };
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
- };
14841
14795
  }, {
14842
14796
  readonly key: "react-doctor/no-ref-current-in-render";
14843
14797
  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 collectInvokedFunctions = (effectCallback, includePromiseCallbacks) => {
13655
+ const collectEffectInvokedFunctions = (effectCallback) => {
13656
13656
  const invokedFunctions = new Set([effectCallback]);
13657
13657
  const localFunctionBindings = /* @__PURE__ */ new Map();
13658
13658
  const calledBindingNames = /* @__PURE__ */ new Set();
@@ -13686,14 +13686,12 @@ const collectInvokedFunctions = (effectCallback, includePromiseCallbacks) => {
13686
13686
  calledBindingNames.add(callee.name);
13687
13687
  return;
13688
13688
  }
13689
- if (includePromiseCallbacks && isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
13689
+ if (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);
13697
13695
  //#endregion
13698
13696
  //#region src/plugin/utils/is-react-hook-name.ts
13699
13697
  const isReactHookName = (name) => {
@@ -14414,7 +14412,7 @@ const findSingleDirectInvocation = (functionNode, caller, context) => {
14414
14412
  const callNode = findDirectCallForReference(reference.identifier);
14415
14413
  return callNode ? [callNode] : [];
14416
14414
  });
14417
- if (invocationCalls.length !== 1 || symbol.references.length !== 1) return null;
14415
+ if (invocationCalls.length !== 1) return null;
14418
14416
  const invocationCall = invocationCalls[0];
14419
14417
  return findEnclosingFunction$1(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
14420
14418
  };
@@ -14701,108 +14699,7 @@ const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, con
14701
14699
  });
14702
14700
  return hasPotentialInterruption;
14703
14701
  };
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
- };
14804
14702
  const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
14805
- if (hasGuardedRefOwnedNestedCleanup(callback, usage, cleanupReturns, context)) return true;
14806
14703
  const usageFunction = findEnclosingFunction$1(usage.node);
14807
14704
  const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
14808
14705
  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;
@@ -15014,7 +14911,6 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
15014
14911
  if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
15015
14912
  if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
15016
14913
  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;
15018
14914
  const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
15019
14915
  if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
15020
14916
  const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
@@ -15051,8 +14947,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
15051
14947
  const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
15052
14948
  if (!releaseHandler) return releaseVerbName === "off";
15053
14949
  const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
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);
14950
+ return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey;
15056
14951
  }
15057
14952
  if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
15058
14953
  return true;
@@ -15077,188 +14972,13 @@ const isPotentiallyReachableFunction = (functionNode, context) => {
15077
14972
  if (!symbol) return false;
15078
14973
  return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
15079
14974
  };
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
- };
15253
14975
  const isReleaseReachableForUsage = (releaseNode, usage, context) => {
15254
14976
  if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
15255
14977
  const releaseFunction = findEnclosingFunction$1(releaseNode);
15256
14978
  if (!releaseFunction) return true;
15257
14979
  if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
15258
- if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
15259
14980
  const usageFunction = findEnclosingFunction$1(usage.node);
15260
14981
  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;
15262
14982
  return isPotentiallyReachableFunction(releaseFunction, context);
15263
14983
  };
15264
14984
  const fileContainsReleaseForUsage = (usage, context) => {
@@ -17392,7 +17112,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
17392
17112
  keys.add(depKey);
17393
17113
  continue;
17394
17114
  }
17395
- const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
17115
+ const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
17396
17116
  if (identitySourceKeys) {
17397
17117
  if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
17398
17118
  for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
@@ -17475,161 +17195,6 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
17475
17195
  if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
17476
17196
  return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
17477
17197
  };
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
- };
17633
17198
  const isUseCallbackResultDep = (node, scopes) => {
17634
17199
  const rootSymbol = getRootSymbol(node, scopes);
17635
17200
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
@@ -42457,8 +42022,7 @@ const noJsxElementType = defineRule({
42457
42022
  create: (context) => {
42458
42023
  let isJsxImported = false;
42459
42024
  const flaggedAnnotations = [];
42460
- const collectComponentReturnType = (functionNode, returnType) => {
42461
- if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
42025
+ const checkReturnType = (returnType) => {
42462
42026
  const typeAnnotation = extractReturnTypeAnnotation(returnType);
42463
42027
  if (!typeAnnotation) return;
42464
42028
  if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
@@ -42468,16 +42032,19 @@ const noJsxElementType = defineRule({
42468
42032
  if (isJsxImportBinding(node)) isJsxImported = true;
42469
42033
  },
42470
42034
  FunctionDeclaration(node) {
42471
- collectComponentReturnType(node, node.returnType);
42035
+ checkReturnType(node.returnType);
42472
42036
  },
42473
42037
  ArrowFunctionExpression(node) {
42474
- collectComponentReturnType(node, node.returnType);
42038
+ checkReturnType(node.returnType);
42475
42039
  },
42476
42040
  FunctionExpression(node) {
42477
- collectComponentReturnType(node, node.returnType);
42041
+ checkReturnType(node.returnType);
42478
42042
  },
42479
42043
  TSDeclareFunction(node) {
42480
- collectComponentReturnType(node, node.returnType);
42044
+ checkReturnType(node.returnType);
42045
+ },
42046
+ TSMethodSignature(node) {
42047
+ checkReturnType(node.returnType);
42481
42048
  },
42482
42049
  "Program:exit"() {
42483
42050
  if (isJsxImported) return;
@@ -47701,69 +47268,6 @@ const noRedundantShouldComponentUpdate = defineRule({
47701
47268
  }
47702
47269
  });
47703
47270
  //#endregion
47704
- //#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
47705
- const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
47706
- const expression = stripParenExpression(rawExpression);
47707
- if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
47708
- if (isNodeOfType(expression, "ConditionalExpression")) {
47709
- if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
47710
- return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
47711
- }
47712
- if (isNodeOfType(expression, "LogicalExpression")) {
47713
- if (isNodeOfType(expression.left, "Literal")) {
47714
- const isLeftTruthy = Boolean(expression.left.value);
47715
- if (expression.operator === "&&" && !isLeftTruthy) return [];
47716
- if (expression.operator === "||" && isLeftTruthy) return [];
47717
- if (expression.operator === "??" && expression.left.value !== null) return [];
47718
- }
47719
- if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
47720
- return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
47721
- }
47722
- if (isNodeOfType(expression, "SequenceExpression")) {
47723
- const finalExpression = expression.expressions.at(-1);
47724
- return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
47725
- }
47726
- if (isNodeOfType(expression, "CallExpression")) {
47727
- if (!isReactApiCall(expression, "useCallback", scopes)) return [];
47728
- const callback = expression.arguments[0];
47729
- return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
47730
- }
47731
- if (!isNodeOfType(expression, "Identifier")) return [];
47732
- const symbol = scopes.symbolFor(expression);
47733
- if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
47734
- 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]));
47735
- const initializer = getDirectConstInitializer(symbol);
47736
- if (!initializer) return [];
47737
- return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
47738
- };
47739
- const functionReturnsCleanupFunction = (functionExpression, scopes) => {
47740
- if (!isFunctionLike$1(functionExpression)) return false;
47741
- if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
47742
- return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
47743
- };
47744
- const callbackReturnsCleanupFunction = (callback, scopes) => {
47745
- return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
47746
- };
47747
- const noRefCallbackCleanupBeforeReact19 = defineRule({
47748
- id: "no-ref-callback-cleanup-before-react-19",
47749
- title: "Ref cleanup requires React 19",
47750
- requires: ["react:18"],
47751
- disabledWhen: ["react:19"],
47752
- severity: "warn",
47753
- 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.",
47754
- create: (context) => ({ JSXAttribute(node) {
47755
- if (getJsxAttributeName(node.name) !== "ref") return;
47756
- if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
47757
- const callback = node.value.expression;
47758
- if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
47759
- if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
47760
- context.report({
47761
- node,
47762
- 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."
47763
- });
47764
- } })
47765
- });
47766
- //#endregion
47767
47271
  //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
47768
47272
  const REPEATED_ANCESTOR_TYPES = new Set([
47769
47273
  "DoWhileStatement",
@@ -72531,17 +72035,6 @@ const reactDoctorRules = [
72531
72035
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
72532
72036
  }
72533
72037
  },
72534
- {
72535
- key: "react-doctor/no-ref-callback-cleanup-before-react-19",
72536
- id: "no-ref-callback-cleanup-before-react-19",
72537
- source: "react-doctor",
72538
- originallyExternal: false,
72539
- rule: {
72540
- ...noRefCallbackCleanupBeforeReact19,
72541
- framework: "global",
72542
- category: "Bugs"
72543
- }
72544
- },
72545
72038
  {
72546
72039
  key: "react-doctor/no-ref-current-in-render",
72547
72040
  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.093619c",
3
+ "version": "0.7.9-dev.0ff57fa",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",