File: RestoreCommand\Diagnostics\UnresolvedMessages.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.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.DependencyResolver;
using NuGet.LibraryModel;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;

namespace NuGet.Commands
{
    /// <summary>
    /// Log errors for packages and projects that were missing.
    /// </summary>
    internal static class UnresolvedMessages
    {
        private static readonly NuGetVersion V0 = new NuGetVersion(0, 0, 0);

        /// <summary>
        /// Log errors for missing dependencies.
        /// </summary>
        internal static async Task LogAsync(IEnumerable<IRestoreTargetGraph> graphs, RemoteWalkContext context, CancellationToken token)
        {
            var tasks = graphs.SelectMany(graph => graph.Unresolved.Select(e =>
            GetMessageAsync(
                graph.TargetGraphName,
                e,
                context.FilterDependencyProvidersForLibrary(e),
                context.PackageSourceMapping.IsEnabled,
                context.RemoteLibraryProviders,
                context.CacheContext,
                context.Logger,
                token))).ToArray();

            var messages = await Task.WhenAll(tasks);

            await context.Logger.LogMessagesAsync(DiagnosticUtility.MergeOnTargetGraph(messages));
        }

        internal static async Task LogAsync(IList<DownloadDependencyResolutionResult> downloadDependencyResults, RemoteWalkContext context, CancellationToken token)
        {
            var messageTasks = new List<Task<RestoreLogMessage>>();

            foreach (var ddi in downloadDependencyResults)
            {
                foreach (var unresolved in ddi.Unresolved)
                {
                    messageTasks.Add(GetMessageAsync(
                        ddi.Framework.ToString(),
                        unresolved,
                        context.FilterDependencyProvidersForLibrary(unresolved),
                        context.PackageSourceMapping.IsEnabled,
                        context.RemoteLibraryProviders,
                        context.CacheContext,
                        context.Logger,
                        token));
                }
            }

            var messages = await Task.WhenAll(messageTasks);
            await context.Logger.LogMessagesAsync(DiagnosticUtility.MergeOnTargetGraph(messages));
        }

        /// <summary>
        /// Create a specific error message for the unresolved dependency.
        /// </summary>
        internal static async Task<RestoreLogMessage> GetMessageAsync(string targetGraphName,
            LibraryRange unresolved,
            IList<IRemoteDependencyProvider> applicableRemoteLibraryProviders,
            bool isPackageSourceMappingEnabled,
            IList<IRemoteDependencyProvider> allRemoteLibraryProviders,
            SourceCacheContext sourceCacheContext,
            ILogger logger,
            CancellationToken token)
        {
            // Default to using the generic unresolved error code, this will be overridden later.
            var code = NuGetLogCode.NU1100;
            var message = string.Empty;

            if (unresolved.TypeConstraintAllows(LibraryDependencyTarget.ExternalProject)
                && !unresolved.TypeConstraintAllows(LibraryDependencyTarget.Package))
            {
                // Project
                // Check if the name is a path and if it exists. All project paths should have been normalized and converted to full paths before this.
#if NETCOREAPP
                if (unresolved.Name.IndexOf(Path.DirectorySeparatorChar, StringComparison.Ordinal) > -1 && File.Exists(unresolved.Name))
#else
                if (unresolved.Name.IndexOf(Path.DirectorySeparatorChar) > -1 && File.Exists(unresolved.Name))
#endif
                {
                    // File exists but the dg spec did not contain the spec
                    code = NuGetLogCode.NU1105;
                    message = string.Format(CultureInfo.CurrentCulture, Strings.Error_UnableToFindProjectInfo, unresolved.Name);
                }
                else
                {
                    // Generic missing project error
                    code = NuGetLogCode.NU1104;
                    message = string.Format(CultureInfo.CurrentCulture, Strings.Error_ProjectDoesNotExist, unresolved.Name);
                }
            }
            else if (unresolved.TypeConstraintAllows(LibraryDependencyTarget.Package) && applicableRemoteLibraryProviders.Count > 0)
            {
                // Package
                var range = unresolved.VersionRange ?? VersionRange.All;
                var sourceInfo = await GetSourceInfosForIdAsync(unresolved.Name, applicableRemoteLibraryProviders, sourceCacheContext, logger, token);

                if (sourceInfo.All(static kvp => kvp.Value.Length == 0))
                {
                    // No versions found
                    code = NuGetLogCode.NU1101;
                    var sourceList = string.Join(", ",
                                        sourceInfo.Select(e => e.Key.Name)
                                                  .OrderBy(e => e, StringComparer.OrdinalIgnoreCase));

                    message = string.Format(CultureInfo.CurrentCulture, Strings.Error_NoPackageVersionsExist, unresolved.Name, sourceList);

                    if (isPackageSourceMappingEnabled && applicableRemoteLibraryProviders.Count != allRemoteLibraryProviders.Count)
                    {
                        string sourcesNotConsidered = FormatProviderNames(GetUnusedLibraryProviders(applicableRemoteLibraryProviders, allRemoteLibraryProviders));

                        message += ". " + string.Format(CultureInfo.CurrentCulture,
                            Strings.Log_SourceMappingEnabledNoMatchingPackageSources,
                            sourcesNotConsidered);
                    }
                }
                else
                {
                    // At least one version found
                    var firstLine = string.Empty;
                    var rangeString = range.ToNonSnapshotRange().PrettyPrint();
                    var allVersions = sourceInfo.SelectMany(static e => e.Value);

                    if (!IsPrereleaseAllowed(range) && HasPrereleaseVersionsOnly(range, allVersions))
                    {
                        code = NuGetLogCode.NU1103;
                        firstLine = string.Format(CultureInfo.CurrentCulture, Strings.Error_NoStablePackageVersionsExist, unresolved.Name, rangeString);
                    }
                    else
                    {
                        code = NuGetLogCode.NU1102;
                        firstLine = string.Format(CultureInfo.CurrentCulture, Strings.Error_NoPackageVersionsExistInRange, unresolved.Name, rangeString);
                    }

                    var lines = new List<string>()
                    {
                        firstLine
                    };

                    lines.AddRange(sourceInfo.Select(e => FormatSourceInfo(e, range)));

                    if (isPackageSourceMappingEnabled && allRemoteLibraryProviders.Count != applicableRemoteLibraryProviders.Count)
                    {
                        lines.AddRange(GetUnusedLibraryProviders(applicableRemoteLibraryProviders, allRemoteLibraryProviders)
                            .OrderBy(e => e.Source.Name)
                            .Select(packageSource => string.Format(CultureInfo.CurrentCulture,
                                            Strings.SourceNotConsidered,
                                            packageSource.Source.Name)));
                    }

                    message = DiagnosticUtility.GetMultiLineMessage(lines);
                }
            }
            else
            {
                // Unknown or non-specific.
                // Also shown when no sources exist.
                message = string.Format(CultureInfo.CurrentCulture,
                    Strings.Log_UnresolvedDependency,
                    unresolved.ToString(),
                    targetGraphName);

                if (isPackageSourceMappingEnabled && applicableRemoteLibraryProviders.Count != allRemoteLibraryProviders.Count)
                {
                    message += " " + string.Format(CultureInfo.CurrentCulture,
                            Strings.Log_SourceMappingEnabledNoMatchingPackageSources,
                            FormatProviderNames(allRemoteLibraryProviders));
                }

                // Set again for clarity
                code = NuGetLogCode.NU1100;
            }

            return RestoreLogMessage.CreateError(code, message, unresolved.Name, targetGraphName);
        }

        private static IEnumerable<IRemoteDependencyProvider> GetUnusedLibraryProviders(IList<IRemoteDependencyProvider> applicableRemoteLibraryProviders, IList<IRemoteDependencyProvider> allRemoteLibraryProviders)
        {
            return allRemoteLibraryProviders
                    .Where(e => !applicableRemoteLibraryProviders.Contains(e))
                    .Select(e => e);
        }

        private static string FormatProviderNames(IEnumerable<IRemoteDependencyProvider> allRemoteLibraryProviders)
        {
            return string.Join(", ", allRemoteLibraryProviders.Select(e => e.Source.Name)
                    .OrderBy(e => e, StringComparer.OrdinalIgnoreCase));
        }

        /// <summary>
        /// True if no stable versions satisfy the range
        /// but a pre-release version is found.
        /// </summary>
        internal static bool HasPrereleaseVersionsOnly(VersionRange range, IEnumerable<NuGetVersion> versions)
        {
            return versions.Any(e => e.IsPrerelease && range.Satisfies(e))
                   && !versions.Any(e => !e.IsPrerelease && range.Satisfies(e));
        }

        /// <summary>
        /// True if the range allows pre-release versions.
        /// </summary>
        internal static bool IsPrereleaseAllowed(VersionRange range)
        {
            return range.MaxVersion?.IsPrerelease == true
                   || range.MinVersion?.IsPrerelease == true;
        }

        /// <summary>
        /// Found 2839 version(s) in nuget-build [ Nearest version: 1.0.0-beta ]
        /// </summary>
        internal static string FormatSourceInfo(KeyValuePair<PackageSource, ImmutableArray<NuGetVersion>> sourceInfo, VersionRange range)
        {
            var bestMatch = GetBestMatch(sourceInfo.Value, range);

            if (bestMatch != null)
            {
                return string.Format(CultureInfo.CurrentCulture,
                    Strings.FoundVersionsInSource,
                    sourceInfo.Value.Length,
                    sourceInfo.Key.Name,
                    bestMatch.ToNormalizedString());
            }

            return string.Format(CultureInfo.CurrentCulture,
                                Strings.FoundVersionsInSourceWithoutMatch,
                                sourceInfo.Value.Length,
                                sourceInfo.Key.Name);
        }

        /// <summary>
        /// Get the complete set of source info for a package id.
        /// </summary>
        internal static async Task<List<KeyValuePair<PackageSource, ImmutableArray<NuGetVersion>>>> GetSourceInfosForIdAsync(
            string id,
            IList<IRemoteDependencyProvider> remoteLibraryProviders,
            SourceCacheContext sourceCacheContext,
            ILogger logger,
            CancellationToken token)
        {
            var sources = new List<KeyValuePair<PackageSource, ImmutableArray<NuGetVersion>>>();

            // Get versions from all sources. These should be cached by the providers already.
            var tasks = remoteLibraryProviders
                .Select(e => GetSourceInfoForIdAsync(e, id, sourceCacheContext, logger, token))
                .ToArray();

            foreach (var task in tasks)
            {
                sources.Add(await task);
            }

            // Sort by most package versions, then by source path.
            return sources.OrderByDescending(e => e.Value.Length)
                .ThenBy(e => e.Key.Source, StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        /// <summary>
        /// Find all package versions from a source.
        /// </summary>
        internal static async Task<KeyValuePair<PackageSource, ImmutableArray<NuGetVersion>>> GetSourceInfoForIdAsync(
            IRemoteDependencyProvider provider,
            string id,
            SourceCacheContext cacheContext,
            ILogger logger,
            CancellationToken token)
        {
            // Find all versions from a source.
            var versions = await provider.GetAllVersionsAsync(id, cacheContext, logger, token);

            return new KeyValuePair<PackageSource, ImmutableArray<NuGetVersion>>(
                provider.Source,
                versions != null ? [.. versions] : ImmutableArray<NuGetVersion>.Empty);
        }

        /// <summary>
        /// Find the best match on the feed.
        /// </summary>
        internal static NuGetVersion? GetBestMatch(ImmutableArray<NuGetVersion> versions, VersionRange range)
        {
            if (versions.Length == 0)
            {
                return null;
            }

            // Find a pivot point
            var ideal = V0;
            NuGetVersion? bestMatch = null;
            if (range.HasLowerBound)
            {
                ideal = range.MinVersion;
            }
            else if (range.HasUpperBound)
            {
                ideal = range.MaxVersion;
            }

            //|      Range     |          Available         | Closest  |
            //| [1.0.0, )      | 0.7.0, 0.9.0               | 0.7.0    |
            //| (0.5.0, 1.0.0) | 0.1.0, 1.0.0               | 1.0.0    |
            //| (, 1.0.0)      | 2.0.0, 3.0.0               | 2.0.0    |
            //| [1.*,)         | 0.0.1, 0.0.5, 0.1.0, 0.9.0 | 0.9.0    |
            //| [1.*, 2.0.0]   | 0.1.0, 0.3.0, 3.0.0, 4.0.0 | 3.0.0    |
            //| *              | 0.0.1-alpha, 2.1.0-p1      | 2.1.0-p1 |
            //Floatless ranges - If there's a pivot/lower or higher bound, take the first above that pivot.
            bool floatlessRangeHasBounds = !range.IsFloating && (range.HasLowerBound || range.HasUpperBound);
            //Floating ranges - by definition they need the latest version, so unless there's an upper bound always show the highest version possible.
            bool floatingRangeHasUpperBound = range.IsFloating && range.HasUpperBound;

            if (floatlessRangeHasBounds || floatingRangeHasUpperBound)
            {
                foreach (var version in versions)
                {
                    if (version == ideal)
                    {
                        return version;
                    }

                    if (version > ideal)
                    {
                        if (bestMatch == null || version < bestMatch)
                        {
                            bestMatch = version;
                        }
                    }
                }
            }

            if (bestMatch == null)
            {
                // Take the highest possible version.
                bestMatch = versions.Max();
            }

            return bestMatch;
        }
    }
}