@@ -895,25 +895,33 @@ private async Task<Dictionary<LibraryDependencyIndex, ResolvedDependencyGraphIte
895895 // Cache the result of IsNewerThanNET10 since it is invariant for the entire method
896896 bool enablePruningWarnings = IsNewerThanNET10 ( projectTargetFramework . FrameworkName ) ;
897897
898- // Used to start over when a dependency has multiple descendants of an item to be evicted.
898+ // A restart is needed when evicting one item invalidates a previous eviction (a cascade eviction).
899+ //
900+ // Consider this graph:
899901 //
900902 // Project
901903 // ├── A 1.0.0
902- // │ └── B 1.0.0
903- // │ └── C 1.0.0
904- // │ └── D 1.0.0
905- // └── X 2.0.0
906- // └── Y 2.0.0
907- // └── G 2.0.0
908- // └── B 2.0.0
904+ // │ └── Q 1.0.0
905+ // └── B 1.0.0
906+ // └── Q 2.0.0
907+ // └── C 2.0.0
908+ // └── B 2.0.0
909+ //
909910 // The items are processed in the following order:
910- // Chose A 1.0.0 and X 1.0.0
911- // Chose B 1.0.0 and Y 2.0.0
912- // Chose C 1.0.0 and G 2 .0.0
913- // Chose D 1.0.0 and B 2.0.0, but B 2.0.0 should evict C 1.0.0 and D 1.0.0
911+ // A 1.0.0 and B 1.0.0 and C 2.0.0 are chosen.
912+ // Q 1.0.0 is chosen initially, but Q 2.0.0 (from B 1.0.0's subtree) evicts it.
913+ // evictions[Q1] records a winning path through B 1 .0.0.
914+ // B 2.0.0 (from C 2.0.0's subtree) evicts B 1.0.0.
914915 //
915- // In this case, the entire walk is started over and B 1.0.0 is left out of the graph, leading to C 1.0.0 and D 1.0.0 also being left out.
916+ // Since Q 2.0.0's winning path went through B 1.0.0, which is now itself evicted,
917+ // Q 1.0.0's eviction is no longer valid and must be reconsidered.
918+ // Because Q 1.0.0 was already dequeued and rejected, the walk must restart from the beginning.
919+ // On the next pass, evictions[B1] prevents B 1.0.0 from being processed, so Q 2.0.0 is never
920+ // reached and Q 1.0.0 is chosen without competition.
916921 //
922+ // Transitive children of an evicted item that are already in resolvedDependencyGraphItems (for
923+ // example C and D in B 1.0.0's subtree) are handled without a restart by removing them directly
924+ // from resolvedDependencyGraphItems; items still in the queue are skipped by the lazy path check.
917925 StartOver :
918926 restartCount ++ ;
919927
@@ -931,7 +939,6 @@ private async Task<Dictionary<LibraryDependencyIndex, ResolvedDependencyGraphIte
931939
932940 GraphItem < RemoteResolveResult > currentGraphItem = await currentDependencyGraphItem . GetGraphItemAsync ( _request . Project . RestoreMetadata , projectTargetFramework . PackagesToPrune , enablePruningWarnings , isRootProject , targetGraphName , _logger ) ;
933941
934- LibraryDependencyTarget typeConstraint = currentDependencyGraphItem . LibraryDependency . LibraryRange . TypeConstraint ;
935942 if ( evictions . TryGetValue ( currentDependencyGraphItem . LibraryRangeIndex , out ( LibraryRangeIndex [ ] Path , LibraryDependencyIndex DependencyIndex , LibraryDependencyTarget TypeConstraint ) eviction ) )
936943 {
937944 // If we evicted this same version previously, but the type constraint of currentRef is more stringent (package), then do not skip the current item - this is the one we want.
@@ -1059,14 +1066,28 @@ private async Task<Dictionary<LibraryDependencyIndex, ResolvedDependencyGraphIte
10591066 // Record an eviction for the item we are replacing. The eviction path is for the current item.
10601067 LibraryRangeIndex evictedLibraryRangeIndex = chosenResolvedItem . LibraryRangeIndex ;
10611068
1062- bool shouldStartOver = false ;
1069+ // Record the eviction immediately so that:
1070+ // 1. The evicted range is skipped if re-encountered at dequeue time (LibraryRangeIndex key check).
1071+ // 2. Items still in the queue whose path passes through the evicted item are lazily skipped.
1072+ evictions [ evictedLibraryRangeIndex ] = ( DependencyGraphItemIndexer . CreatePathToRef ( currentDependencyGraphItem . Path , currentDependencyGraphItem . LibraryRangeIndex ) , currentDependencyGraphItem . LibraryDependencyIndex , chosenResolvedItem . LibraryDependency . LibraryRange . TypeConstraint ) ;
10631073
1064- // To "evict" a chosen item, we need to also remove all of its transitive children from the chosen list.
1074+ // Check whether any previously-recorded evictions have a winning path that passes through
1075+ // the item we just evicted. Those evictions are now invalid because the ancestor that caused
1076+ // them is itself evicted. The previously-rejected items must be re-evaluated, but they are
1077+ // no longer in the queue, so the entire walk must restart.
10651078 HashSet < LibraryRangeIndex > ? evicteesToRemove = default ;
10661079
10671080 foreach ( KeyValuePair < LibraryRangeIndex , ( LibraryRangeIndex [ ] Path , LibraryDependencyIndex DependencyIndex , LibraryDependencyTarget TypeConstraint ) > evictee in evictions )
10681081 {
1069- // See if the evictee is a descendant of the evicted item
1082+ // Skip the eviction entry we just recorded above; it was added before this loop
1083+ // started, so the dictionary enumerator will reach it, but it is not a pre-existing
1084+ // eviction and its path will never contain evictedLibraryRangeIndex by definition.
1085+ if ( evictee . Key == evictedLibraryRangeIndex )
1086+ {
1087+ continue ;
1088+ }
1089+
1090+ // See if the evictee's winning path passes through the item we just evicted
10701091 if ( evictee . Value . Path . Contains ( evictedLibraryRangeIndex ) )
10711092 {
10721093 // if evictee.Key (depIndex) == currentDepIndex && evictee.TypeConstraint == ExternalProject --> Don't remove it. It must remain evicted.
@@ -1086,28 +1107,35 @@ private async Task<Dictionary<LibraryDependencyIndex, ResolvedDependencyGraphIte
10861107 foreach ( LibraryRangeIndex evicteeToRemove in evicteesToRemove )
10871108 {
10881109 evictions . Remove ( evicteeToRemove ) ;
1089-
1090- // Indicate that we can't simply evict this item and instead we need to start over knowing that this item should be skipped
1091- shouldStartOver = true ;
10921110 }
1111+
1112+ // The previously-rejected items that are now un-evicted are no longer in the queue,
1113+ // so the walk must restart from the beginning.
1114+ goto StartOver ;
10931115 }
10941116
1095- foreach ( KeyValuePair < LibraryDependencyIndex , ResolvedDependencyGraphItem > chosenItem in resolvedDependencyGraphItems )
1117+ // No cascade eviction requires a restart. Remove any already-resolved items whose ancestry
1118+ // passes through the evicted item; they are stale because the evicted item is no longer in
1119+ // the graph. Items still in the queue that are children of the evicted item will be skipped
1120+ // lazily by the path check at dequeue time.
1121+ List < LibraryDependencyIndex > ? resolvedItemsToRemove = null ;
1122+
1123+ foreach ( KeyValuePair < LibraryDependencyIndex , ResolvedDependencyGraphItem > resolvedItem in resolvedDependencyGraphItems )
10961124 {
1097- if ( chosenItem . Value . Path . Contains ( evictedLibraryRangeIndex ) )
1125+ if ( resolvedItem . Value . Path . Contains ( evictedLibraryRangeIndex ) )
10981126 {
1099- // Indicate that we can't simply evict this item and instead we need to start over knowing that this item should be skipped
1100- shouldStartOver = true ;
1101- break ;
1127+ resolvedItemsToRemove ??= new List < LibraryDependencyIndex > ( capacity : 4 ) ;
1128+
1129+ resolvedItemsToRemove . Add ( resolvedItem . Key ) ;
11021130 }
11031131 }
11041132
1105- // Add the eviction to be used later; children of the evicted item that remain in the queue will be skipped lazily via the path check above
1106- evictions [ evictedLibraryRangeIndex ] = ( DependencyGraphItemIndexer . CreatePathToRef ( currentDependencyGraphItem . Path , currentDependencyGraphItem . LibraryRangeIndex ) , currentDependencyGraphItem . LibraryDependencyIndex , chosenResolvedItem . LibraryDependency . LibraryRange . TypeConstraint ) ;
1107-
1108- if ( shouldStartOver )
1133+ if ( resolvedItemsToRemove != null )
11091134 {
1110- goto StartOver ;
1135+ foreach ( LibraryDependencyIndex key in resolvedItemsToRemove )
1136+ {
1137+ resolvedDependencyGraphItems . Remove ( key ) ;
1138+ }
11111139 }
11121140
11131141 // Add the item to the list of chosen items
0 commit comments