File: Commands\PackageReferenceCommands\ListPackage\ListPackageCommand.cs
Web Access
Project: src\nuget-client\src\NuGet.Core\NuGet.CommandLine.XPlat\NuGet.CommandLine.XPlat.csproj (NuGet.CommandLine.XPlat)
// 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.

#nullable enable

using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using NuGet.CommandLine.XPlat.ListPackage;
using NuGet.Commands;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Credentials;

namespace NuGet.CommandLine.XPlat
{
    internal static class ListPackageCommand
    {
        internal static void Register(
            Command parent,
            Func<ILoggerWithColor> getLogger,
            Action<LogLevel> setLogLevel,
            Func<IListPackageCommandRunner> getCommandRunner,
            TextWriter? consoleOut = null,
            TextWriter? consoleError = null)
        {
            var listCommand = new Command("list", Strings.ListPkg_Description);

            // The .NET SDK always resolves and forwards a concrete project/solution path to this command
            // (both `dotnet package list` and the classic `dotnet list package` route through the SDK's
            // PackageListCommand, which defaults to the current directory and resolves it to a file before
            // invoking NuGet). ExactlyOne therefore only affects direct invocation of this executable, where
            // omitting the path previously threw an ArgumentNullException; it now produces a clean parse error.
            var path = new Argument<string>("<PROJECT | SOLUTION>")
            {
                Description = Strings.ListPkg_PathDescription,
                Arity = ArgumentArity.ExactlyOne
            };

            var framework = new Option<string[]>("--framework")
            {
                Description = Strings.ListPkg_FrameworkDescription,
                Arity = ArgumentArity.OneOrMore
            };

            var deprecatedReport = new Option<bool>("--deprecated")
            {
                Description = Strings.ListPkg_DeprecatedDescription,
                Arity = ArgumentArity.Zero
            };

            var outdatedReport = new Option<bool>("--outdated")
            {
                Description = Strings.ListPkg_OutdatedDescription,
                Arity = ArgumentArity.Zero
            };

            var vulnerableReport = new Option<bool>("--vulnerable")
            {
                Description = Strings.ListPkg_VulnerableDescription,
                Arity = ArgumentArity.Zero
            };

            var includeTransitive = new Option<bool>("--include-transitive")
            {
                Description = Strings.ListPkg_TransitiveDescription,
                Arity = ArgumentArity.Zero
            };

            var prerelease = new Option<bool>("--include-prerelease")
            {
                Description = Strings.ListPkg_PrereleaseDescription,
                Arity = ArgumentArity.Zero
            };

            var highestPatch = new Option<bool>("--highest-patch")
            {
                Description = Strings.ListPkg_HighestPatchDescription,
                Arity = ArgumentArity.Zero
            };

            var highestMinor = new Option<bool>("--highest-minor")
            {
                Description = Strings.ListPkg_HighestMinorDescription,
                Arity = ArgumentArity.Zero
            };

            var source = new Option<string[]>("--source")
            {
                Description = Strings.ListPkg_SourceDescription,
                Arity = ArgumentArity.OneOrMore
            };

            var config = new Option<string>("--config")
            {
                Description = Strings.ListPkg_ConfigDescription,
                Arity = ArgumentArity.ZeroOrOne
            };

            var outputFormat = new Option<string>("--format")
            {
                Description = Strings.ListPkg_OutputFormatDescription,
                Arity = ArgumentArity.ZeroOrOne
            };

            var outputVersion = new Option<string>("--output-version")
            {
                Description = Strings.ListPkg_OutputVersionDescription,
                Arity = ArgumentArity.ZeroOrOne
            };

            var interactive = new Option<bool>("--interactive")
            {
                Description = Strings.NuGetXplatCommand_Interactive,
                Arity = ArgumentArity.Zero
            };

            var verbosity = new Option<string>("--verbosity", "-v")
            {
                Description = Strings.Verbosity_Description,
                Arity = ArgumentArity.ZeroOrOne
            };

            listCommand.Arguments.Add(path);
            listCommand.Options.Add(framework);
            listCommand.Options.Add(deprecatedReport);
            listCommand.Options.Add(outdatedReport);
            listCommand.Options.Add(vulnerableReport);
            listCommand.Options.Add(includeTransitive);
            listCommand.Options.Add(prerelease);
            listCommand.Options.Add(highestPatch);
            listCommand.Options.Add(highestMinor);
            listCommand.Options.Add(source);
            listCommand.Options.Add(config);
            listCommand.Options.Add(outputFormat);
            listCommand.Options.Add(outputVersion);
            listCommand.Options.Add(interactive);
            listCommand.Options.Add(verbosity);

            listCommand.SetAction(async (parseResult, cancellationToken) =>
            {
                var logger = getLogger();

                var verbosityValue = parseResult.GetValue(verbosity) ?? string.Empty;
                setLogLevel(XPlatUtility.MSBuildVerbosityToNuGetLogLevel(verbosityValue));

                var pathValue = parseResult.GetValue(path) ?? string.Empty;
                var configValue = parseResult.GetValue(config);
                var hasConfig = !string.IsNullOrEmpty(configValue);

                var settings = ProcessConfigFile(configValue, pathValue);
                var sourceValues = parseResult.GetValue(source) ?? Array.Empty<string>();

                var packageSources = GetPackageSources(settings, sourceValues, hasConfig);

                var reportType = GetReportType(
                    isOutdated: parseResult.GetValue(outdatedReport),
                    isDeprecated: parseResult.GetValue(deprecatedReport),
                    isVulnerable: parseResult.GetValue(vulnerableReport));

                IReportRenderer reportRenderer = GetOutputType(consoleOut ?? Console.Out, consoleError ?? Console.Error, parseResult.GetValue(outputFormat), outputVersionOption: parseResult.GetValue(outputVersion));
                var provider = new PackageSourceProvider(settings);
                var frameworkValues = parseResult.GetValue(framework) ?? Array.Empty<string>();
                var packageRefArgs = new ListPackageArgs(
                    pathValue,
                    packageSources,
                    frameworkValues.ToList(),
                    reportType,
                    reportRenderer,
                    parseResult.GetValue(includeTransitive),
                    parseResult.GetValue(prerelease),
                    parseResult.GetValue(highestPatch),
                    parseResult.GetValue(highestMinor),
                    provider.LoadAuditSources(),
                    logger,
                    CancellationToken.None);

                WarnAboutIncompatibleOptions(packageRefArgs, reportRenderer);

                DefaultCredentialServiceUtility.SetupDefaultCredentialService(getLogger(), !parseResult.GetValue(interactive));

                var listPackageCommandRunner = getCommandRunner();
                return await listPackageCommandRunner.ExecuteCommandAsync(packageRefArgs);
            });

            parent.Subcommands.Add(listCommand);
        }

        private static ReportType GetReportType(bool isDeprecated, bool isOutdated, bool isVulnerable)
        {
            var mutexCount = 0;
            mutexCount += isDeprecated ? 1 : 0;
            mutexCount += isOutdated ? 1 : 0;
            mutexCount += isVulnerable ? 1 : 0;
            if (mutexCount == 0)
            {
                return ReportType.Default;
            }
            else if (mutexCount == 1)
            {
                return isDeprecated ? ReportType.Deprecated : isOutdated ? ReportType.Outdated : ReportType.Vulnerable;
            }

            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_InvalidOptions));
        }

        private static IReportRenderer GetOutputType(TextWriter consoleOut, TextWriter consoleError, string? outputFormatOption, string? outputVersionOption)
        {
            ReportOutputFormat outputFormat = ReportOutputFormat.Console;
            if (!string.IsNullOrEmpty(outputFormatOption) &&
                !Enum.TryParse(outputFormatOption, ignoreCase: true, out outputFormat))
            {
                string currentlySupportedFormat = GetEnumValues<ReportOutputFormat>();
                throw new ArgumentException(string.Format(Strings.ListPkg_InvalidOutputFormat, outputFormatOption, currentlySupportedFormat));
            }

            if (outputFormat == ReportOutputFormat.Console)
            {
                if (!string.IsNullOrEmpty(outputVersionOption))
                {
                    throw new ArgumentException(string.Format(Strings.ListPkg_OutputVersionNotApplicable));
                }
                return new ListPackageConsoleRenderer(consoleOut, consoleError);
            }

            IReportRenderer jsonReportRenderer;

            var currentlySupportedReportVersions = new List<string> { "1" };
            if (!string.IsNullOrEmpty(outputVersionOption) && !currentlySupportedReportVersions.Contains(outputVersionOption))
            {
                throw new ArgumentException(string.Format(Strings.ListPkg_InvalidOutputVersion, outputVersionOption, string.Join(" ,", currentlySupportedReportVersions)));
            }
            else
            {
                jsonReportRenderer = new ListPackageJsonRenderer(consoleOut);
            }

            return jsonReportRenderer;
        }

        private static void WarnAboutIncompatibleOptions(ListPackageArgs packageRefArgs, IReportRenderer reportRenderer)
        {
            if (packageRefArgs.ReportType != ReportType.Outdated &&
                (packageRefArgs.Prerelease || packageRefArgs.HighestMinor || packageRefArgs.HighestPatch))
            {
                reportRenderer.AddProblem(ProblemType.Warning, Strings.ListPkg_VulnerableIgnoredOptions);
            }
        }

        private static ISettings ProcessConfigFile(string? configFile, string? projectOrSolution)
        {
            if (string.IsNullOrEmpty(configFile))
            {
                return Settings.LoadDefaultSettings(projectOrSolution);
            }

            var configFileFullPath = Path.GetFullPath(configFile);
            var directory = Path.GetDirectoryName(configFileFullPath);
            var configFileName = Path.GetFileName(configFileFullPath);
            return Settings.LoadDefaultSettings(
                directory,
                configFileName,
                machineWideSettings: new XPlatMachineWideSetting());
        }

        private static List<PackageSource> GetPackageSources(ISettings settings, IEnumerable<string> sources, bool hasConfig)
        {
            var availableSources = PackageSourceProvider.LoadPackageSources(settings).Where(source => source.IsEnabled);
            var uniqueSources = new HashSet<string>();

            var packageSources = new List<PackageSource>();
            foreach (var source in sources)
            {
                if (!uniqueSources.Contains(source))
                {
                    uniqueSources.Add(source);
                    packageSources.Add(PackageSourceProviderExtensions.ResolveSource(availableSources, source));
                }
            }

            if (packageSources.Count == 0 || hasConfig)
            {
                packageSources.AddRange(availableSources);
            }

            return packageSources;
        }

        private static string GetEnumValues<T>() where T : struct, Enum
        {
            var enumValues = Enum.GetValues<T>()
               .Select(x => x.ToString());

            return string.Join(", ", enumValues).ToLower(CultureInfo.CurrentCulture);
        }
    }
}