@@ -40,6 +40,9 @@ import { getFinalSequenceExpressionValue } from "../../utils/get-final-sequence-
4040import { doNodesCoverEveryPathAfterNode } from "../../utils/do-nodes-cover-every-path-after-node.js" ;
4141import { doNodesCoverEveryPathFromFunctionEntry } from "../../utils/do-nodes-cover-every-path-from-function-entry.js" ;
4242import { getFunctionBindingIdentifier } from "../../utils/get-function-binding-name.js" ;
43+ import { getEquivalentSymbols } from "../../utils/get-equivalent-symbols.js" ;
44+ import { getSingleReturnExpression } from "../../utils/get-single-return-expression.js" ;
45+ import { functionReturnsMatchingExpressionOnEveryPathAfterNode } from "../../utils/function-returns-matching-expression.js" ;
4346import { getImportDeclarationForSymbol } from "../../utils/get-import-declaration-for-symbol.js" ;
4447import { getRangeStart } from "../../utils/get-range-start.js" ;
4548import { getStaticPropertyKeyName } from "../../utils/get-static-property-key-name.js" ;
@@ -1064,6 +1067,31 @@ const findAssignedResourceKey = (
10641067 const parentNode = currentNode . parent ;
10651068 if ( isNodeOfType ( parentNode , "VariableDeclarator" ) && parentNode . init === currentNode ) {
10661069 const localKey = resolveExpressionKey ( parentNode . id , context ) ;
1070+ const symbol = isNodeOfType ( parentNode . id , "Identifier" )
1071+ ? context . scopes . symbolFor ( parentNode . id )
1072+ : null ;
1073+ const factory = findEnclosingFunction ( currentNode ) ;
1074+ const caller = factory ? findEnclosingFunction ( factory ) : null ;
1075+ if (
1076+ symbol ?. kind === "const" &&
1077+ isFunctionLike ( factory ) &&
1078+ ! factory . async &&
1079+ ! factory . generator &&
1080+ factory . params . length === 0 &&
1081+ caller &&
1082+ functionReturnsMatchingExpressionOnEveryPathAfterNode (
1083+ factory ,
1084+ currentNode ,
1085+ context . scopes ,
1086+ ( expression ) => context . scopes . symbolFor ( stripParenExpression ( expression ) ) === symbol ,
1087+ context . cfg ,
1088+ )
1089+ ) {
1090+ const invocation = findSingleDirectInvocation ( factory , caller , context ) ;
1091+ if ( isNodeOfType ( invocation , "CallExpression" ) && invocation . arguments . length === 0 ) {
1092+ return findAssignedResourceKey ( invocation , context , includeRetainedStorage ) ?? localKey ;
1093+ }
1094+ }
10671095 if ( ! includeRetainedStorage ) return localKey ;
10681096 const retainedStorage = findRetainedResourceStorage ( resourceNode , context ) ;
10691097 return retainedStorage ?. key ?? localKey ;
@@ -1079,6 +1107,23 @@ const findAssignedResourceKey = (
10791107 }
10801108 return resolveExpressionKey ( parentNode . left , context ) ;
10811109 }
1110+ if ( isNodeOfType ( parentNode , "ReturnStatement" ) && parentNode . argument === currentNode ) {
1111+ const factory = findEnclosingFunction ( currentNode ) ;
1112+ const caller = factory ? findEnclosingFunction ( factory ) : null ;
1113+ if (
1114+ isFunctionLike ( factory ) &&
1115+ ! factory . async &&
1116+ ! factory . generator &&
1117+ factory . params . length === 0 &&
1118+ getSingleReturnExpression ( factory ) === currentNode &&
1119+ caller
1120+ ) {
1121+ const invocation = findSingleDirectInvocation ( factory , caller , context ) ;
1122+ if ( isNodeOfType ( invocation , "CallExpression" ) && invocation . arguments . length === 0 ) {
1123+ return findAssignedResourceKey ( invocation , context , includeRetainedStorage ) ;
1124+ }
1125+ }
1126+ }
10821127 return includeRetainedStorage
10831128 ? ( resolveRetainedResourceStorage ( currentNode , context ) ?. key ?? null )
10841129 : null ;
@@ -1699,11 +1744,19 @@ const removeSynchronouslyReleasedUsages = (
16991744 }
17001745 if ( ! isNodeOfType ( callback . body , "BlockStatement" ) ) return usages ;
17011746 const releaseCalls : EsTreeNode [ ] = [ ] ;
1702- walkInsideStatementBlocks ( callback . body , ( child : EsTreeNode ) => {
1703- const callNode = isNodeOfType ( child , "ChainExpression" ) ? child . expression : child ;
1704- if ( ! isNodeOfType ( callNode , "CallExpression" ) ) return ;
1705- releaseCalls . push ( child ) ;
1706- } ) ;
1747+ for ( const invokedFunction of collectSynchronouslyEffectInvokedFunctions (
1748+ callback ,
1749+ context . scopes ,
1750+ ) ) {
1751+ if ( ! isFunctionLike ( invokedFunction ) ) continue ;
1752+ if ( invokedFunction !== callback && ( invokedFunction . async || invokedFunction . generator ) )
1753+ continue ;
1754+ walkInsideStatementBlocks ( invokedFunction . body , ( child : EsTreeNode ) => {
1755+ const callNode = isNodeOfType ( child , "ChainExpression" ) ? child . expression : child ;
1756+ if ( ! isNodeOfType ( callNode , "CallExpression" ) ) return ;
1757+ releaseCalls . push ( child ) ;
1758+ } ) ;
1759+ }
17071760 if ( releaseCalls . length === 0 ) return usages ;
17081761 return usages . filter ( ( usage ) => {
17091762 const usageStart = getRangeStart ( usage . node ) ;
@@ -2799,13 +2852,14 @@ const findSingleDirectInvocation = (
27992852 if ( ! bindingIdentifier || resolveStableValue ( bindingIdentifier , context ) !== functionNode ) {
28002853 return null ;
28012854 }
2802- const symbol = context . scopes . symbolFor ( bindingIdentifier ) ;
2803- if ( ! symbol ) return null ;
2804- const invocationCalls = symbol . references . flatMap ( ( reference ) => {
2855+ const references = getEquivalentSymbols ( bindingIdentifier , context . scopes ) . flatMap (
2856+ ( symbol ) => symbol . references ,
2857+ ) ;
2858+ const invocationCalls = references . flatMap ( ( reference ) => {
28052859 const callNode = findDirectCallForReference ( reference . identifier ) ;
28062860 return callNode ? [ callNode ] : [ ] ;
28072861 } ) ;
2808- if ( invocationCalls . length !== 1 || symbol . references . length !== 1 ) return null ;
2862+ if ( invocationCalls . length !== 1 || references . length !== 1 ) return null ;
28092863 const invocationCall = invocationCalls [ 0 ] ;
28102864 return findEnclosingFunction ( invocationCall ) === caller &&
28112865 isNodeReachableWithinFunction ( invocationCall , context )
@@ -3410,13 +3464,7 @@ const callbackReturnsCleanupForUsage = (
34103464 context : RuleContext ,
34113465 isReactRefCallback = false ,
34123466) : boolean => {
3413- if (
3414- ! isNodeOfType ( callback , "ArrowFunctionExpression" ) &&
3415- ! isNodeOfType ( callback , "FunctionExpression" )
3416- ) {
3417- return false ;
3418- }
3419- if ( callback . async ) return false ;
3467+ if ( ! isFunctionLike ( callback ) || callback . async || callback . generator ) return false ;
34203468 const doesReturnedValueReleaseUsage = ( returnedValue : EsTreeNode ) : boolean => {
34213469 if ( doesBoundCleanupReleaseUsage ( returnedValue , usage , context ) ) return true ;
34223470 const stableCleanupValue = resolveStableValue ( returnedValue , context ) ;
@@ -6718,13 +6766,36 @@ const effectHasCleanupForUsage = (
67186766 findEnclosingFunction ( assignedHandleSymbol . declarationNode ) === callback ,
67196767 ) ) ) ;
67206768 const matchingCleanupReturns : EsTreeNode [ ] = [ ] ;
6769+ let hasReturnedFactoryCleanup = false ;
67216770 walkInsideStatementBlocks ( callback . body , ( child : EsTreeNode ) => {
67226771 if ( ! isNodeOfType ( child , "ReturnStatement" ) ) return ;
67236772 const returnStart = getRangeStart ( child ) ;
67246773 const usageStart = getRangeStart ( usage . node ) ;
6725- if ( returnStart !== null && usageStart !== null && returnStart < usageStart ) return ;
67266774 const returnedValue = child . argument ? stripParenExpression ( child . argument ) : null ;
67276775 if ( ! returnedValue ) return ;
6776+ if ( isNodeOfType ( returnedValue , "CallExpression" ) && returnedValue . arguments . length === 0 ) {
6777+ const factory = resolveStableValue ( returnedValue . callee , context ) ;
6778+ if (
6779+ isFunctionLike ( factory ) &&
6780+ factory . params . length === 0 &&
6781+ findEnclosingFunction ( factory ) === callback &&
6782+ findEnclosingFunction ( usage . node ) === factory &&
6783+ callbackReturnsCleanupForUsage ( factory , usage , context )
6784+ ) {
6785+ const factoryReferences : EsTreeNode [ ] = [ ] ;
6786+ walkAst ( callback , ( candidate ) => {
6787+ const reference = context . scopes . referenceFor ( candidate ) ;
6788+ if ( reference && resolveStableValue ( candidate , context ) === factory ) {
6789+ factoryReferences . push ( candidate ) ;
6790+ }
6791+ } ) ;
6792+ if ( factoryReferences . length === 1 && factoryReferences [ 0 ] === returnedValue . callee ) {
6793+ hasReturnedFactoryCleanup = true ;
6794+ return ;
6795+ }
6796+ }
6797+ }
6798+ if ( returnStart !== null && usageStart !== null && returnStart < usageStart ) return ;
67286799 if ( doesBoundCleanupReleaseUsage ( returnedValue , usage , context ) ) {
67296800 matchingCleanupReturns . push ( child ) ;
67306801 return ;
@@ -6772,6 +6843,7 @@ const effectHasCleanupForUsage = (
67726843 matchingCleanupReturns . push ( child ) ;
67736844 }
67746845 } ) ;
6846+ if ( hasReturnedFactoryCleanup ) return true ;
67756847 if (
67766848 hasOwnedObserverHelperCleanup ( callback , usage , allUsages , matchingCleanupReturns , context ) ||
67776849 hasGuardedDeferredCleanup ( callback , usage , matchingCleanupReturns , context )
0 commit comments