File: RestoreCommand\DependencyGraphResolver.DependencyGraphItem.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.DependencyResolver;
using NuGet.LibraryModel;
using NuGet.ProjectModel;

namespace NuGet.Commands
{
    internal sealed partial class DependencyGraphResolver
    {
        /// <summary>
        /// A class representing a declared dependency graph item to consider for the resolved graph.
        /// </summary>
        [DebuggerDisplay("{LibraryDependency}, DependencyIndex={LibraryDependencyIndex}, RangeIndex={LibraryRangeIndex}")]
        public class DependencyGraphItem
        {
            /// <summary>
            /// Gets or initializes a <see cref="Task{TResult}" /> that returns a <see cref="GraphItem{TItem}" /> containing a <see cref="RemoteResolveResult" /> that represents the resolved graph item after looking it up in the configured feeds.
            /// </summary>
            public required Task<GraphItem<RemoteResolveResult>> FindLibraryTask { get; init; }

            /// <summary>
            /// Gets or initializes a value indicating whether or not this dependency graph item is a transitively pinned dependency.
            /// </summary>
            public bool IsCentrallyPinnedTransitivePackage { get; init; }

            /// <summary>
            /// Gets or initializes a value indicating whether or not this dependency graph item is a direct package reference from the root project.
            /// </summary>
            public bool IsRootPackageReference { get; init; }

            /// <summary>
            /// Gets or initializes the <see cref="LibraryDependency" /> used to declare this dependency graph item.
            /// </summary>
            public required LibraryDependency LibraryDependency { get; init; }

            /// <summary>
            /// Gets or initializes the <see cref="LibraryDependencyIndex" /> associated with this dependency graph item.
            /// </summary>
            public LibraryDependencyIndex LibraryDependencyIndex { get; init; }

            /// <summary>
            /// Gets or initializes the <see cref="LibraryRangeIndex" /> associated with this dependency graph item.
            /// </summary>
            public LibraryRangeIndex LibraryRangeIndex { get; init; }

            /// <summary>
            /// Gets or initializes the <see cref="LibraryRangeIndex" /> of this dependency graph item's parent.
            /// </summary>
            public LibraryRangeIndex Parent { get; init; }

            /// <summary>
            /// Gets or initializes an array representing all of the parent <see cref="LibraryRangeIndex" /> values of this dependency graph item.
            /// </summary>
            public LibraryRangeIndex[] Path { get; init; } = Array.Empty<LibraryRangeIndex>();

            /// <summary>
            /// Gets or initializes a <see cref="HashSet{T}" /> containing <see cref="LibraryDependency" /> objects representing any runtime specific dependencies.
            /// </summary>
            public HashSet<LibraryDependency>? RuntimeDependencies { get; init; }

            /// <summary>
            /// Gets or initializes a <see cref="HashSet{T}" /> containing <see cref="LibraryDependencyIndex" /> values representing any libraries that should be suppressed under this dependency graph item.
            /// </summary>
            public HashSet<LibraryDependencyIndex>? Suppressions { get; init; }

            /// <summary>
            /// Gets the <see cref="GraphItem{TItem}" /> with the <see cref="RemoteResolveResult" /> of this dependency graph item from calling the delegate specified in <see cref="FindLibraryTask" />.
            /// </summary>
            /// <param name="projectRestoreMetadata">The <see cref="ProjectRestoreMetadata" /> of the current restore.</param>
            /// <param name="packagesToPrune">An optional <see cref="IReadOnlyDictionary{TKey, TValue}" /> containing any packages to prune from the defined dependencies.</param>
            /// <param name="isRootProject">Indicates whether or not the dependency graph item is the root project.</param>
            /// <param name="logger">An <see cref="ILogger" /> used for logging.</param>
            /// <returns>A <see cref="GraphItem{TItem}" /> with the <see cref="RemoteResolveResult" /> of the result of finding the library.</returns>
            public async Task<GraphItem<RemoteResolveResult>> GetGraphItemAsync(
                ProjectRestoreMetadata projectRestoreMetadata,
                IReadOnlyDictionary<string, PrunePackageReference>? packagesToPrune,
                bool enablePruningWarnings,
                bool isRootProject,
                string targetGraphName,
                ILogger logger)
            {
                // Call the task to get the library, this may returned a cached result
                GraphItem<RemoteResolveResult> item = await FindLibraryTask;

                // If there are no runtime dependencies or packages to prune, just return the original result
                if ((RuntimeDependencies == null || RuntimeDependencies.Count == 0) && (packagesToPrune == null || packagesToPrune.Count == 0))
                {
                    return item;
                }

                // Create a new list that is big enough for the existing items plus any runtime dependencies
                List<LibraryDependency> dependencies = new(capacity: (RuntimeDependencies?.Count ?? 0) + item.Data.Dependencies.Count);

                // Loop through the defined dependencies, leaving out any pruned packages or runtime packages that replace them
                for (int i = 0; i < item.Data.Dependencies.Count; i++)
                {
                    LibraryDependency dependency = item.Data.Dependencies[i];

                    // Skip any packages that should be pruned or will be replaced with a runtime dependency
                    if (ShouldPrunePackage(projectRestoreMetadata, packagesToPrune, enablePruningWarnings, dependency, item.Key, isRootProject, targetGraphName, logger)
                        || RuntimeDependencies?.Contains(dependency) == true)
                    {
                        continue;
                    }

                    dependencies.Add(dependency);
                }

                if (RuntimeDependencies?.Count > 0)
                {
                    // Add any runtime dependencies unless they should be pruned
                    foreach (LibraryDependency runtimeDependency in RuntimeDependencies)
                    {
                        if (ShouldPrunePackage(projectRestoreMetadata, packagesToPrune, enablePruningWarnings, runtimeDependency, item.Key, isRootProject, targetGraphName, logger) == true)
                        {
                            continue;
                        }

                        dependencies.Add(runtimeDependency);
                    }
                }

                // Return a new graph item containing the mutated list of dependencies
                return new GraphItem<RemoteResolveResult>(item.Key)
                {
                    Data = new RemoteResolveResult()
                    {
                        Dependencies = dependencies,
                        Match = item.Data.Match
                    }
                };
            }

            /// <summary>
            /// Determines whether or not a package should be pruned from the list of defined dependencies.
            /// </summary>
            /// <remarks>
            /// A package is not pruned if it is a project reference or a direct package reference from the root project.
            /// </remarks>
            /// <param name="projectRestoreMetadata">The <see cref="ProjectRestoreMetadata" /> of the current restore.</param>
            /// <param name="packagesToPrune">An optional <see cref="IReadOnlyDictionary{TKey, TValue}" /> containing any packages to prune from the defined dependencies.</param>
            /// <param name="dependency">The <see cref="LibraryDependency" /> of the defined dependency.</param>
            /// <param name="parentLibrary">The <see cref="LibraryIdentity" /> of the parent library that defined this dependency.</param>
            /// <param name="isRootProject">Indicates if the parent library is the root project.</param>
            /// <param name="logger">An <see cref="ILogger" /> used for logging.</param>
            /// <returns><see langword="true" /> if the package should be pruned, otherwise <see langword="false" />.</returns>
            private static bool ShouldPrunePackage(
                ProjectRestoreMetadata projectRestoreMetadata,
                IReadOnlyDictionary<string, PrunePackageReference>? packagesToPrune,
                bool enablePruningWarnings,
                LibraryDependency dependency,
                LibraryIdentity parentLibrary,
                bool isRootProject,
                string targetGraphName,
                ILogger logger)
            {
                if (packagesToPrune?.TryGetValue(dependency.Name, out PrunePackageReference? packageToPrune) != true
                    || !dependency.LibraryRange!.VersionRange!.Satisfies(packageToPrune!.VersionRange!.MaxVersion!))
                {
                    // There is either no packages to prune or the package to prune does not match the version range of the dependency
                    return false;
                }

                bool isPackage = dependency.LibraryRange?.TypeConstraintAllows(LibraryDependencyTarget.Package) == true;

                if (!isPackage)
                {
                    if (isRootProject && enablePruningWarnings)
                    {
                        logger.Log(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1511, string.Format(CultureInfo.CurrentCulture, Strings.Error_RestorePruningProjectReference, dependency.Name), dependency.Name,
                            targetGraphName));
                    }

                    return false;
                }

                if (isRootProject) // Don't prune direct PRs
                {
                    return false;
                }

                logger.LogDebug(string.Format(CultureInfo.CurrentCulture, Strings.RestoreDebugPruningPackageReference, $"{dependency.Name} {dependency.LibraryRange!.VersionRange.OriginalString}", parentLibrary, packageToPrune.VersionRange.MaxVersion));

                return true;
            }
        }
    }
}