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 +46 -0
- package/dist/index.js +778 -40
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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") &&
|
|
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) &&
|
|
40740
|
+
if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
|
|
40295
40741
|
});
|
|
40296
40742
|
return names;
|
|
40297
40743
|
};
|
|
40298
|
-
const
|
|
40299
|
-
|
|
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
|
|
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, "
|
|
40937
|
-
|
|
40938
|
-
|
|
40939
|
-
|
|
40940
|
-
|
|
40941
|
-
|
|
40942
|
-
|
|
40943
|
-
|
|
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
|
|
40948
|
-
|
|
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(
|
|
41759
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
|
|
41094
41760
|
if (!componentOrHookNode) return;
|
|
41095
41761
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
41096
|
-
if (requiresRenderedContext && !isInRenderedOutput(
|
|
41762
|
+
if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41097
41763
|
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
41098
|
-
const attribute = findEnclosingJsxAttribute(
|
|
41764
|
+
const attribute = findEnclosingJsxAttribute(conditionNode);
|
|
41099
41765
|
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
41100
41766
|
}
|
|
41101
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(
|
|
41102
|
-
if (isAfterClientOnlyEarlyReturn(
|
|
41103
|
-
const openingElement = findEnclosingJsxOpeningElement(
|
|
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
|
|
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
|
-
|
|
42520
|
+
collectComponentReturnType(node, node.returnType);
|
|
41854
42521
|
},
|
|
41855
42522
|
ArrowFunctionExpression(node) {
|
|
41856
|
-
|
|
42523
|
+
collectComponentReturnType(node, node.returnType);
|
|
41857
42524
|
},
|
|
41858
42525
|
FunctionExpression(node) {
|
|
41859
|
-
|
|
42526
|
+
collectComponentReturnType(node, node.returnType);
|
|
41860
42527
|
},
|
|
41861
42528
|
TSDeclareFunction(node) {
|
|
41862
|
-
|
|
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",
|