diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/IListPackageCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/IListPackageCommandRunner.cs index 4df8a339f94..3f34c15570e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/IListPackageCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/IListPackageCommandRunner.cs @@ -7,6 +7,6 @@ namespace NuGet.CommandLine.XPlat { internal interface IListPackageCommandRunner { - Task ExecuteCommandAsync(ListPackageArgs packageRefArgs); + Task ExecuteCommandAsync(ListPackageArgs packageRefArgs); } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageArgs.cs index 26c13c02f04..2013342e884 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageArgs.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using System.Threading; +using NuGet.CommandLine.XPlat.ListPackage; using NuGet.Common; using NuGet.Configuration; @@ -16,6 +19,8 @@ internal class ListPackageArgs public IEnumerable PackageSources { get; } public IEnumerable Frameworks { get; } public ReportType ReportType { get; } + public IReportRenderer Renderer { get; } + public string ArgumentText { get; } public bool IncludeTransitive { get; } public bool Prerelease { get; } public bool HighestPatch { get; } @@ -31,6 +36,7 @@ internal class ListPackageArgs /// The sources for the packages to check in the case of --outdated /// The user inputed frameworks to look up for their packages /// Which report we're producing (e.g. --outdated) + /// The report output renderer (e.g. console, json) /// Bool for --include-transitive present /// Bool for --include-prerelease present /// Bool for --highest-patch present @@ -42,6 +48,7 @@ public ListPackageArgs( IEnumerable packageSources, IEnumerable frameworks, ReportType reportType, + IReportRenderer renderer, bool includeTransitive, bool prerelease, bool highestPatch, @@ -53,12 +60,63 @@ public ListPackageArgs( PackageSources = packageSources ?? throw new ArgumentNullException(nameof(packageSources)); Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks)); ReportType = reportType; + Renderer = renderer; IncludeTransitive = includeTransitive; Prerelease = prerelease; HighestPatch = highestPatch; HighestMinor = highestMinor; Logger = logger ?? throw new ArgumentNullException(nameof(logger)); CancellationToken = cancellationToken; + ArgumentText = GetReportParameters(); + } + + private string GetReportParameters() + { + StringBuilder sb = new StringBuilder(); + + switch (ReportType) + { + case ReportType.Default: + break; + case ReportType.Deprecated: + sb.Append(" --deprecated"); + break; + case ReportType.Outdated: + sb.Append(" --outdated"); + break; + case ReportType.Vulnerable: + sb.Append(" --vulnerable"); + break; + default: + break; + } + + if (IncludeTransitive) + { + sb.Append(" --include-transitive"); + } + + if (Frameworks != null && Frameworks.Any()) + { + sb.Append(" --framework " + string.Join(" ", Frameworks)); + } + + if (Prerelease) + { + sb.Append(" --include-prerelease"); + } + + if (HighestMinor) + { + sb.Append(" --highest-minor"); + } + + if (HighestPatch) + { + sb.Append("--highest-patch"); + } + + return sb.ToString().Trim(); } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommand.cs index be99b2eeb77..5cd9f6ddc63 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommand.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using Microsoft.Extensions.CommandLineUtils; +using NuGet.CommandLine.XPlat.ListPackage; using NuGet.Commands; using NuGet.Common; using NuGet.Configuration; @@ -89,6 +90,16 @@ public static void Register( Strings.ListPkg_ConfigDescription, CommandOptionType.SingleValue); + var outputFormat = listpkg.Option( + "--format", + Strings.ListPkg_OutputFormatDescription, + CommandOptionType.SingleValue); + + var outputVersion = listpkg.Option( + "--output-version", + Strings.ListPkg_OutputVersionDescription, + CommandOptionType.SingleValue); + var interactive = listpkg.Option( "--interactive", Strings.NuGetXplatCommand_Interactive, @@ -117,11 +128,14 @@ public static void Register( isDeprecated: deprecatedReport.HasValue(), isVulnerable: vulnerableReport.HasValue()); + IReportRenderer reportRenderer = GetOutputType(outputFormat.Value(), outputVersionOption: outputVersion.Value()); + var packageRefArgs = new ListPackageArgs( path.Value, packageSources, framework.Values, reportType, + reportRenderer, includeTransitive.HasValue(), prerelease.HasValue(), highestPatch.HasValue(), @@ -129,13 +143,12 @@ public static void Register( logger, CancellationToken.None); - DisplayMessages(packageRefArgs); + WarnAboutIncompatibleOptions(packageRefArgs, reportRenderer); DefaultCredentialServiceUtility.SetupDefaultCredentialService(getLogger(), !interactive.HasValue()); var listPackageCommandRunner = getCommandRunner(); - await listPackageCommandRunner.ExecuteCommandAsync(packageRefArgs); - return 0; + return await listPackageCommandRunner.ExecuteCommandAsync(packageRefArgs); }); }); } @@ -158,12 +171,43 @@ private static ReportType GetReportType(bool isDeprecated, bool isOutdated, bool throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_InvalidOptions)); } - private static void DisplayMessages(ListPackageArgs packageRefArgs) + private static IReportRenderer GetOutputType(string outputFormatOption, string outputVersionOption) + { + ReportOutputFormat outputFormat = ReportOutputFormat.Console; + if (!string.IsNullOrEmpty(outputFormatOption) && + !Enum.TryParse(outputFormatOption, ignoreCase: true, out outputFormat)) + { + string currentlySupportedFormat = GetEnumValues(); + throw new ArgumentException(string.Format(Strings.ListPkg_InvalidOutputFormat, outputFormatOption, currentlySupportedFormat)); + } + + if (outputFormat == ReportOutputFormat.Console) + { + return new ListPackageConsoleRenderer(); + } + + IReportRenderer jsonReportRenderer; + + var currentlySupportedReportVersions = new List { "1" }; + // If customer pass unsupported version then error out instead of defaulting to version probably unsupported by customer machine. + if (!string.IsNullOrEmpty(outputVersionOption) && !currentlySupportedReportVersions.Contains(outputVersionOption)) + { + throw new ArgumentException(string.Format(Strings.ListPkg_InvalidOutputVersion, outputVersionOption, string.Join(" ,", currentlySupportedReportVersions))); + } + else + { + jsonReportRenderer = new ListPackageJsonRenderer(); + } + + return jsonReportRenderer; + } + + private static void WarnAboutIncompatibleOptions(ListPackageArgs packageRefArgs, IReportRenderer reportRenderer) { if (packageRefArgs.ReportType != ReportType.Outdated && (packageRefArgs.Prerelease || packageRefArgs.HighestMinor || packageRefArgs.HighestPatch)) { - Console.WriteLine(Strings.ListPkg_VulnerableIgnoredOptions); + reportRenderer.AddProblem(ProblemType.Warning, Strings.ListPkg_VulnerableIgnoredOptions); } } @@ -216,5 +260,13 @@ private static IEnumerable GetPackageSources(ISettings settings, return packageSources; } + + private static string GetEnumValues() where T : Enum + { + var enumValues = ((T[])Enum.GetValues(typeof(T))) + .Select(x => x.ToString()); + + return string.Join(", ", enumValues).ToLower(CultureInfo.CurrentCulture); + } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs index c0924fc082b..9ef528a6546 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Build.Evaluation; +using NuGet.CommandLine.XPlat.ListPackage; using NuGet.CommandLine.XPlat.Utility; using NuGet.Configuration; using NuGet.Packaging; @@ -22,6 +23,8 @@ internal class ListPackageCommandRunner : IListPackageCommandRunner { private const string ProjectAssetsFile = "ProjectAssetsFile"; private const string ProjectName = "MSBuildProjectName"; + private const int GenericSuccessExitCode = 0; + private const int GenericFailureExitCode = 1; private Dictionary _sourceRepositoryCache; public ListPackageCommandRunner() @@ -29,14 +32,25 @@ public ListPackageCommandRunner() _sourceRepositoryCache = new Dictionary(); } - public async Task ExecuteCommandAsync(ListPackageArgs listPackageArgs) + public async Task ExecuteCommandAsync(ListPackageArgs listPackageArgs) { + IReportRenderer reportRenderer = listPackageArgs.Renderer; + (int exitCode, ListPackageReportModel reportModel) = await GetReportDataAsync(listPackageArgs); + reportRenderer.Render(reportModel); + return exitCode; + } + + internal async Task<(int, ListPackageReportModel)> GetReportDataAsync(ListPackageArgs listPackageArgs) + { + // It's important not to print anything to console from below methods and sub method calls, because it'll affect both json/console outputs. + var listPackageReportModel = new ListPackageReportModel(listPackageArgs); if (!File.Exists(listPackageArgs.Path)) { - Console.Error.WriteLine(string.Format(CultureInfo.CurrentCulture, + listPackageArgs.Renderer.AddProblem(problemType: ProblemType.Error, + text: string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorFileNotFound, listPackageArgs.Path)); - return; + return (GenericFailureExitCode, listPackageReportModel); } //If the given file is a solution, get the list of projects //If not, then it's a project, which is put in a list @@ -44,125 +58,107 @@ public async Task ExecuteCommandAsync(ListPackageArgs listPackageArgs) MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path).Where(f => File.Exists(f)) : new List(new string[] { listPackageArgs.Path }); - var autoReferenceFound = false; - var msBuild = new MSBuildAPIUtility(listPackageArgs.Logger); + MSBuildAPIUtility msBuild = listPackageReportModel.MSBuildAPIUtility; - //Print sources, but not for generic list (which is offline) - if (listPackageArgs.ReportType != ReportType.Default) + foreach (string projectPath in projectsPaths) { - Console.WriteLine(); - Console.WriteLine(Strings.ListPkg_SourcesUsedDescription); - ProjectPackagesPrintUtility.PrintSources(listPackageArgs.PackageSources); - Console.WriteLine(); + await GetProjectMetadataAsync(projectPath, listPackageReportModel, msBuild, listPackageArgs); } - foreach (var projectPath in projectsPaths) - { - //Open project to evaluate properties for the assets - //file and the name of the project - var project = MSBuildAPIUtility.GetProject(projectPath); + // if there is any error then return failure code. + int exitCode = ( + listPackageArgs.Renderer.GetProblems().Any(p => p.ProblemType == ProblemType.Error) + || listPackageReportModel.Projects.Where(p => p.ProjectProblems != null).SelectMany(p => p.ProjectProblems).Any(p => p.ProblemType == ProblemType.Error)) + ? GenericFailureExitCode : GenericSuccessExitCode; - if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) - { - Console.Error.WriteLine(string.Format(CultureInfo.CurrentCulture, - Strings.Error_NotPRProject, - projectPath)); - Console.WriteLine(); - continue; - } + return (exitCode, listPackageReportModel); + } - var projectName = project.GetPropertyValue(ProjectName); + private async Task GetProjectMetadataAsync( + string projectPath, + ListPackageReportModel listPackageReportModel, + MSBuildAPIUtility msBuild, + ListPackageArgs listPackageArgs) + { + //Open project to evaluate properties for the assets + //file and the name of the project + Project project = MSBuildAPIUtility.GetProject(projectPath); + var projectName = project.GetPropertyValue(ProjectName); + ListPackageProjectModel projectModel = listPackageReportModel.CreateProjectReportData(projectPath: projectPath, projectName); - var assetsPath = project.GetPropertyValue(ProjectAssetsFile); + if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) + { + projectModel.AddProjectInformation(problemType: ProblemType.Error, + string.Format(CultureInfo.CurrentCulture, Strings.Error_NotPRProject, projectPath)); + return; + } - // If the file was not found, print an error message and continue to next project - if (!File.Exists(assetsPath)) - { - Console.Error.WriteLine(string.Format(CultureInfo.CurrentCulture, - Strings.Error_AssetsFileNotFound, - projectPath)); - Console.WriteLine(); - } - else + var assetsPath = project.GetPropertyValue(ProjectAssetsFile); + + if (!File.Exists(assetsPath)) + { + projectModel.AddProjectInformation(ProblemType.Error, + string.Format(CultureInfo.CurrentCulture, Strings.Error_AssetsFileNotFound, projectPath)); + } + else + { + var lockFileFormat = new LockFileFormat(); + LockFile assetsFile = lockFileFormat.Read(assetsPath); + + // Assets file validation + if (assetsFile.PackageSpec != null && + assetsFile.Targets != null && + assetsFile.Targets.Count != 0) { - var lockFileFormat = new LockFileFormat(); - var assetsFile = lockFileFormat.Read(assetsPath); + // Get all the packages that are referenced in a project + IEnumerable packages; + try + { + packages = msBuild.GetResolvedVersions(project.FullPath, listPackageArgs.Frameworks, assetsFile, listPackageArgs.IncludeTransitive, includeProjects: listPackageArgs.ReportType == ReportType.Default); + } + catch (InvalidOperationException ex) + { + projectModel.AddProjectInformation(ProblemType.Error, ex.Message); + return; + } - // Assets file validation - if (assetsFile.PackageSpec != null && - assetsFile.Targets != null && - assetsFile.Targets.Count != 0) + if (packages.Any()) { - // Get all the packages that are referenced in a project - var packages = msBuild.GetResolvedVersions(project.FullPath, listPackageArgs.Frameworks, assetsFile, listPackageArgs.IncludeTransitive, includeProjects: listPackageArgs.ReportType == ReportType.Default); + if (listPackageArgs.ReportType != ReportType.Default) // generic list package is offline -- no server lookups + { + PopulateSourceRepositoryCache(listPackageArgs); + WarnForHttpSources(listPackageArgs, projectModel); + await GetRegistrationMetadataAsync(packages, listPackageArgs); + await AddLatestVersionsAsync(packages, listPackageArgs); + } - // If packages equals null, it means something wrong happened - // with reading the packages and it was handled and message printed - // in MSBuildAPIUtility function, but we need to move to the next project - if (packages != null) + bool printPackages = FilterPackages(packages, listPackageArgs); + printPackages = printPackages || ReportType.Default == listPackageArgs.ReportType; + if (printPackages) { - // No packages means that no package references at all were found in the current framework - if (!packages.Any()) - { - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_NoPackagesFoundForFrameworks, projectName)); - } - else - { - if (listPackageArgs.ReportType != ReportType.Default) // generic list package is offline -- no server lookups - { - PopulateSourceRepositoryCache(listPackageArgs); - WarnForHttpSources(listPackageArgs); - await GetRegistrationMetadataAsync(packages, listPackageArgs); - await AddLatestVersionsAsync(packages, listPackageArgs); - } - - bool printPackages = FilterPackages(packages, listPackageArgs); - - // Filter packages for dedicated reports, inform user if none - if (listPackageArgs.ReportType != ReportType.Default && !printPackages) - { - switch (listPackageArgs.ReportType) - { - case ReportType.Outdated: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_NoUpdatesForProject, projectName)); - break; - case ReportType.Deprecated: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_NoDeprecatedPackagesForProject, projectName)); - break; - case ReportType.Vulnerable: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_NoVulnerablePackagesForProject, projectName)); - break; - } - } - - printPackages = printPackages || ReportType.Default == listPackageArgs.ReportType; - if (printPackages) - { - var hasAutoReference = false; - ProjectPackagesPrintUtility.PrintPackages(packages, projectName, listPackageArgs, ref hasAutoReference); - autoReferenceFound = autoReferenceFound || hasAutoReference; - } - } + var hasAutoReference = false; + List projectFrameworkPackages = ProjectPackagesPrintUtility.GetPackagesMetadata(packages, listPackageArgs, ref hasAutoReference); + projectModel.TargetFrameworkPackages = projectFrameworkPackages; + projectModel.AutoReferenceFound = hasAutoReference; + } + else + { + projectModel.TargetFrameworkPackages = new List(); } } - else - { - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingAssetsFile, assetsPath)); - } - - // Unload project - ProjectCollection.GlobalProjectCollection.UnloadProject(project); } - } + else + { + projectModel.AddProjectInformation(ProblemType.Error, + string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingAssetsFile, assetsPath)); + } - // Print a legend message for auto-reference markers used - if (autoReferenceFound) - { - Console.WriteLine(Strings.ListPkg_AutoReferenceDescription); + // Unload project + ProjectCollection.GlobalProjectCollection.UnloadProject(project); } } - private static void WarnForHttpSources(ListPackageArgs listPackageArgs) + private static void WarnForHttpSources(ListPackageArgs listPackageArgs, ListPackageProjectModel projectModel) { List httpPackageSources = null; foreach (PackageSource packageSource in listPackageArgs.PackageSources) @@ -181,7 +177,8 @@ private static void WarnForHttpSources(ListPackageArgs listPackageArgs) { if (httpPackageSources.Count == 1) { - listPackageArgs.Logger.LogWarning( + projectModel.AddProjectInformation( + ProblemType.Warning, string.Format(CultureInfo.CurrentCulture, Strings.Warning_HttpServerUsage, "list package", @@ -189,7 +186,8 @@ private static void WarnForHttpSources(ListPackageArgs listPackageArgs) } else { - listPackageArgs.Logger.LogWarning( + projectModel.AddProjectInformation( + ProblemType.Warning, string.Format(CultureInfo.CurrentCulture, Strings.Warning_HttpServerUsage_MultipleSources, "list package", diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/GlobalSuppressions.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/GlobalSuppressions.cs index 752a3779f99..a6f901af42f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/GlobalSuppressions.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/GlobalSuppressions.cs @@ -14,9 +14,6 @@ [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void CommandOutputLogger.LogInternal(LogLevel logLevel, string message)', validate parameter 'message' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.CommandOutputLogger.LogInternal(NuGet.Common.LogLevel,System.String)")] [assembly: SuppressMessage("Build", "CA1307:The behavior of 'string.IndexOf(char)' could vary based on the current user's locale settings. Replace this call in 'NuGet.CommandLine.XPlat.CommandOutputLogger.LogInternal(NuGet.Common.LogLevel, string)' with a call to 'string.IndexOf(char, System.StringComparison)'.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.CommandOutputLogger.LogInternal(NuGet.Common.LogLevel,System.String)")] [assembly: SuppressMessage("Build", "CA1822:Member AddPackagesToDict does not access instance data and can be marked as static (Shared in VisualBasic)", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommandRunner.AddPackagesToDict(System.Collections.Generic.IEnumerable{NuGet.CommandLine.XPlat.FrameworkPackages},System.Collections.Generic.Dictionary{System.String,System.Collections.Generic.IList{NuGet.Protocol.Core.Types.IPackageSearchMetadata}})")] -[assembly: SuppressMessage("Build", "CA1307:The behavior of 'string.Equals(string)' could vary based on the current user's locale settings. Replace this call in 'NuGet.CommandLine.XPlat.ListPackageCommandRunner.ExecuteCommandAsync(NuGet.CommandLine.XPlat.ListPackageArgs)' with a call to 'string.Equals(string, System.StringComparison)'.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommandRunner.ExecuteCommandAsync(NuGet.CommandLine.XPlat.ListPackageArgs)~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'Task ListPackageCommandRunner.ExecuteCommandAsync(ListPackageArgs listPackageArgs)', validate parameter 'listPackageArgs' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommandRunner.ExecuteCommandAsync(NuGet.CommandLine.XPlat.ListPackageArgs)~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Build", "CA1822:Member GetLatestVersionPerSourceAsync does not access instance data and can be marked as static (Shared in VisualBasic)", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommandRunner.GetLatestVersionPerSourceAsync(NuGet.Configuration.PackageSource,NuGet.CommandLine.XPlat.ListPackageArgs,System.String,System.Collections.Generic.IEnumerable{System.Lazy{NuGet.Protocol.Core.Types.INuGetResourceProvider}},System.Collections.Generic.Dictionary{System.String,System.Collections.Generic.IList{NuGet.Protocol.Core.Types.IPackageSearchMetadata}})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Build", "CA1822:Member GetUpdateLevel does not access instance data and can be marked as static (Shared in VisualBasic)", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommandRunner.GetUpdateLevel(NuGet.Versioning.NuGetVersion,NuGet.Versioning.NuGetVersion)~NuGet.CommandLine.XPlat.UpdateLevel")] [assembly: SuppressMessage("Build", "CA1822:Member MeetsConstraints does not access instance data and can be marked as static (Shared in VisualBasic)", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.ListPackageCommandRunner.MeetsConstraints(NuGet.Versioning.NuGetVersion,NuGet.CommandLine.XPlat.InstalledPackageReference,NuGet.CommandLine.XPlat.ListPackageArgs)~System.Boolean")] [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'void MSBuildAPIUtility.AddPackageReferencePerTFM(string projectPath, LibraryDependency libraryDependency, IEnumerable frameworks)', validate parameter 'libraryDependency' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.MSBuildAPIUtility.AddPackageReferencePerTFM(System.String,NuGet.LibraryModel.LibraryDependency,System.Collections.Generic.IEnumerable{System.String})")] @@ -30,7 +27,3 @@ [assembly: SuppressMessage("Build", "CA1062:In externally visible method 'Task RemovePackageReferenceCommandRunner.ExecuteCommand(PackageReferenceArgs packageReferenceArgs, MSBuildAPIUtility msBuild)', validate parameter 'msBuild' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "", Scope = "member", Target = "~M:NuGet.CommandLine.XPlat.RemovePackageReferenceCommandRunner.ExecuteCommand(NuGet.CommandLine.XPlat.PackageReferenceArgs,NuGet.CommandLine.XPlat.MSBuildAPIUtility)~System.Threading.Tasks.Task{System.Int32}")] [assembly: SuppressMessage("Build", "CA1819:Properties should not return arrays", Justification = "", Scope = "member", Target = "~P:NuGet.CommandLine.XPlat.PackageReferenceArgs.Frameworks")] [assembly: SuppressMessage("Build", "CA1819:Properties should not return arrays", Justification = "", Scope = "member", Target = "~P:NuGet.CommandLine.XPlat.PackageReferenceArgs.Sources")] -[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "External source file injected via NuGet contentFiles. Cannot be modified", Scope = "member", Target = "~M:Microsoft.Extensions.CommandLineUtils.CommandLineApplication.GetHelpText(System.String)~System.String")] -[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "External source file injected via NuGet contentFiles. Cannot be modified", Scope = "member", Target = "~M:Microsoft.Extensions.CommandLineUtils.CommandLineApplication.Argument(System.String,System.String,System.Action{Microsoft.Extensions.CommandLineUtils.CommandArgument},System.Boolean)~Microsoft.Extensions.CommandLineUtils.CommandArgument")] -[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "External source file injected via NuGet contentFiles. Cannot be modified", Scope = "member", Target = "~M:Microsoft.Extensions.CommandLineUtils.CommandLineApplication.GetFullNameAndVersion~System.String")] -[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "External source file injected via NuGet contentFiles. Cannot be modifieds", Scope = "member", Target = "~M:Microsoft.Extensions.CommandLineUtils.CommandLineApplication.ShowHint")] diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/IReportRenderer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/IReportRenderer.cs new file mode 100644 index 00000000000..e8cbb21c235 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/IReportRenderer.cs @@ -0,0 +1,14 @@ +// 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.Collections.Generic; + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + internal interface IReportRenderer + { + void AddProblem(ProblemType problemType, string text); + IEnumerable GetProblems(); + void Render(ListPackageReportModel reportProject); + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageConsoleRenderer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageConsoleRenderer.cs new file mode 100644 index 00000000000..8b4a349955a --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageConsoleRenderer.cs @@ -0,0 +1,228 @@ +// 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.Globalization; +using System.Linq; +using NuGet.CommandLine.XPlat.Utility; +using NuGet.Configuration; + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + /// + /// Console output renderer for dotnet list package command + /// + internal class ListPackageConsoleRenderer : IReportRenderer + { + protected List _problems = new(); + + public ListPackageConsoleRenderer() + { } + + public void AddProblem(ProblemType problemType, string text) + { + _problems.Add(new ReportProblem(problemType, string.Empty, text)); + } + + public IEnumerable GetProblems() + { + return _problems; + } + + public void Render(ListPackageReportModel listPackageReportModel) + { + WriteToConsole(listPackageReportModel); + } + + private void WriteToConsole(ListPackageReportModel listPackageReportModel) + { + // Print non-project related problems first. + PrintProblems(_problems, listPackageReportModel.ListPackageArgs); + + if (_problems?.Any(p => p.ProblemType == ProblemType.Error) == true) + { + return; + } + + WriteSources(listPackageReportModel.ListPackageArgs); + WriteProjects(listPackageReportModel.Projects, listPackageReportModel.ListPackageArgs); + + // Print a legend message for auto-reference markers used + if (listPackageReportModel.Projects.Any(p => p.AutoReferenceFound)) + { + Console.WriteLine(Strings.ListPkg_AutoReferenceDescription); + } + } + + private static void WriteSources(ListPackageArgs listPackageArgs) + { + // Print sources, but not for generic list (which is offline) + if (listPackageArgs.ReportType != ReportType.Default) + { + Console.WriteLine(); + Console.WriteLine(Strings.ListPkg_SourcesUsedDescription); + PrintSources(listPackageArgs.PackageSources); + Console.WriteLine(); + } + } + + private static void WriteProjects(List projects, ListPackageArgs listPackageArgs) + { + foreach (ListPackageProjectModel project in projects) + { + PrintProblems(project.ProjectProblems, listPackageArgs); + + if (project.ProjectProblems?.Any(p => p.ProblemType == ProblemType.Error) == true) + { + return; + } + + if (project.TargetFrameworkPackages == null) + { + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_NoPackagesFoundForFrameworks, project.ProjectName)); + continue; + } + + bool printPackages = project.TargetFrameworkPackages.Any(p => p.TopLevelPackages?.Any() == true || + listPackageArgs.IncludeTransitive && p.TransitivePackages?.Any() == true); + + // Filter packages for dedicated reports, inform user if none + if (listPackageArgs.ReportType != ReportType.Default && !printPackages) + { + switch (listPackageArgs.ReportType) + { + case ReportType.Outdated: + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_NoUpdatesForProject, project.ProjectName)); + break; + case ReportType.Deprecated: + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_NoDeprecatedPackagesForProject, project.ProjectName)); + break; + case ReportType.Vulnerable: + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_NoVulnerablePackagesForProject, project.ProjectName)); + break; + } + } + + printPackages = printPackages || ReportType.Default == listPackageArgs.ReportType; + if (!printPackages) + { + continue; + } + + Console.WriteLine(GetProjectHeader(project.ProjectName, listPackageArgs)); + + foreach (ListPackageReportFrameworkPackage frameworkPackages in project.TargetFrameworkPackages) + { + List frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; + List frameworkTransitivePackages = frameworkPackages.TransitivePackages; + + // If no packages exist for this framework, print the + // appropriate message + if (frameworkTopLevelPackages?.Any() != true && frameworkTransitivePackages?.Any() != true) + { + Console.ForegroundColor = ConsoleColor.Blue; + + switch (listPackageArgs.ReportType) + { + case ReportType.Outdated: + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: " + Strings.ListPkg_NoUpdatesForFramework, frameworkPackages.Framework)); + break; + case ReportType.Deprecated: + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: " + Strings.ListPkg_NoDeprecationsForFramework, frameworkPackages.Framework)); + break; + case ReportType.Vulnerable: + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: " + Strings.ListPkg_NoVulnerabilitiesForFramework, frameworkPackages.Framework)); + break; + case ReportType.Default: + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: " + Strings.ListPkg_NoPackagesForFramework, frameworkPackages.Framework)); + break; + } + + Console.ResetColor(); + } + else + { + // Print name of the framework + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: ", frameworkPackages.Framework)); + Console.ResetColor(); + + // Print top-level packages + if (frameworkTopLevelPackages?.Any() == true) + { + var tableHasAutoReference = false; + var tableToPrint = ProjectPackagesPrintUtility.BuildPackagesTable( + frameworkTopLevelPackages, printingTransitive: false, listPackageArgs, ref tableHasAutoReference); + if (tableToPrint != null) + { + ProjectPackagesPrintUtility.PrintPackagesTable(tableToPrint); + } + } + + // Print transitive packages + if (listPackageArgs.IncludeTransitive && frameworkTransitivePackages?.Any() == true) + { + var tableHasAutoReference = false; + var tableToPrint = ProjectPackagesPrintUtility.BuildPackagesTable( + frameworkTransitivePackages, printingTransitive: true, listPackageArgs, ref tableHasAutoReference); + if (tableToPrint != null) + { + ProjectPackagesPrintUtility.PrintPackagesTable(tableToPrint); + } + } + } + } + } + } + + private static void PrintSources(IEnumerable packageSources) + { + foreach (var source in packageSources) + { + Console.WriteLine(" " + source.Source); + } + } + + private static void PrintProblems(IEnumerable problems, ListPackageArgs listPackageArgs) + { + if (problems == null) + { + return; + } + + foreach (ReportProblem problem in problems) + { + switch (problem.ProblemType) + { + case ProblemType.Warning: + listPackageArgs.Logger.LogWarning(problem.Text); + break; + case ProblemType.Error: + Console.Error.WriteLine(problem.Text); + Console.WriteLine(); + break; + default: + break; + } + } + } + + private static string GetProjectHeader(string projectName, ListPackageArgs listPackageArgs) + { + switch (listPackageArgs.ReportType) + { + case ReportType.Outdated: + return string.Format(Strings.ListPkg_ProjectUpdatesHeaderLog, projectName); + case ReportType.Deprecated: + return string.Format(Strings.ListPkg_ProjectDeprecationsHeaderLog, projectName); + case ReportType.Vulnerable: + return string.Format(Strings.ListPkg_ProjectVulnerabilitiesHeaderLog, projectName); + case ReportType.Default: + break; + } + + return string.Format(Strings.ListPkg_ProjectHeaderLog, projectName); + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageJsonRenderer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageJsonRenderer.cs new file mode 100644 index 00000000000..cae331db421 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageJsonRenderer.cs @@ -0,0 +1,362 @@ +// 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.IO; +using Newtonsoft.Json; +using System.Linq; +using NuGet.Common; +using NuGet.Versioning; +using NuGet.Protocol; +using NuGet.Configuration; + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + /// + /// json format renderer for dotnet list package command + /// + internal class ListPackageJsonRenderer : IReportRenderer + { + private const int ReportOutputVersion = 1; + private const string VersionProperty = "version"; + private const string ParametersProperty = "parameters"; + private const string ProblemsProperty = "problems"; + private const string SourcesProperty = "sources"; + private const string ProjectProperty = "project"; + private const string ProjectsProperty = "projects"; + private const string FrameworksProperty = "frameworks"; + private const string FrameworkProperty = "framework"; + private const string PathProperty = "path"; + private const string TopLevelPackagesProperty = "topLevelPackages"; + private const string TransitivePackagesProperty = "transitivePackages"; + private const string IdProperty = "id"; + private const string RequestedVersionProperty = "requestedVersion"; + private const string ResolvedVersionProperty = "resolvedVersion"; + private const string AutoReferencedProperty = "autoReferenced"; + private const string SeverityProperty = "severity"; + private const string AdvisoryUrlProperty = "advisoryurl"; + private const string VulnerabilitiesProperty = "vulnerabilities"; + private const string LatestVersionProperty = "latestVersion"; + private const string DeprecationReasonsProperty = "deprecationReasons"; + private const string AlternativePackageProperty = "alternativePackage"; + private const string VersionRangeProperty = "versionRange"; + private const string LevelProperty = "level"; + private const string WarningProperty = "warning"; + private const string TextProperty = "text"; + private const string ErrorProperty = "error"; + + protected readonly List _problems = new(); + protected TextWriter _writer; + + private ListPackageJsonRenderer() + { } + + public ListPackageJsonRenderer(TextWriter textWriter = null) + { + _writer = textWriter != null ? textWriter : Console.Out; + } + + public void AddProblem(ProblemType problemType, string text) + { + _problems.Add(new ReportProblem(problemType, string.Empty, text)); + } + + public IEnumerable GetProblems() + { + return _problems; + } + + public void Render(ListPackageReportModel listPackageReportModel) + { + // Aggregate problems from projects. + _problems.AddRange(listPackageReportModel.Projects.Where(p => p.ProjectProblems != null).SelectMany(p => p.ProjectProblems)); + var jsonRenderedOutput = WriteJson(listPackageReportModel); + _writer.WriteLine(jsonRenderedOutput); + } + + internal string WriteJson(ListPackageReportModel listPackageReportModel) + { + using (var writer = new StringWriter()) + { + using (var jsonWriter = new JsonTextWriter(writer)) + { + jsonWriter.Formatting = Formatting.Indented; + WriteJson(jsonWriter, listPackageReportModel); + } + + return writer.ToString(); + } + } + + private void WriteJson(JsonWriter writer, ListPackageReportModel listPackageReportModel) + { + ListPackageArgs listPackageArgs = listPackageReportModel.ListPackageArgs; + writer.WriteStartObject(); + + writer.WritePropertyName(VersionProperty); + writer.WriteValue(ReportOutputVersion); + + writer.WritePropertyName(ParametersProperty); + writer.WriteValue(PathUtility.GetPathWithForwardSlashes(listPackageArgs.ArgumentText)); + + if (listPackageReportModel.Projects.Any(p => p.AutoReferenceFound)) + { + _problems.Add(new ReportProblem(ProblemType.Warning, string.Empty, Strings.ListPkg_AutoReferenceDescription)); + } + + if (_problems?.Count > 0) + { + WriteProblems(writer, _problems); + } + + WriteSources(writer, listPackageReportModel.ListPackageArgs); + WriteProjects(writer, listPackageReportModel.Projects, listPackageReportModel.ListPackageArgs); + writer.WriteEndObject(); + } + + private static void WriteProblems(JsonWriter writer, IEnumerable reportProblems) + { + writer.WritePropertyName(ProblemsProperty); + writer.WriteStartArray(); + + foreach (ReportProblem reportProblem in reportProblems) + { + writer.WriteStartObject(); + + if (!string.IsNullOrEmpty(reportProblem.Project)) + { + writer.WritePropertyName(ProjectProperty); + writer.WriteValue(PathUtility.GetPathWithForwardSlashes(reportProblem.Project)); + } + + writer.WritePropertyName(LevelProperty); + writer.WriteValue(reportProblem.ProblemType == ProblemType.Error ? ErrorProperty : WarningProperty); + writer.WritePropertyName(TextProperty); + writer.WriteValue(PathUtility.GetPathWithForwardSlashes(reportProblem.Text)); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + private static void WriteSources(JsonWriter writer, ListPackageArgs listPackageArgs) + { + if (listPackageArgs.ReportType == ReportType.Default) + { + // generic list is offline. + return; + } + + writer.WritePropertyName(SourcesProperty); + writer.WriteStartArray(); + + foreach (PackageSource packageSource in listPackageArgs.PackageSources) + { + writer.WriteValue(PathUtility.GetPathWithForwardSlashes(packageSource.Source)); + } + + writer.WriteEndArray(); + } + + private static void WriteProjects(JsonWriter writer, List reportProjects, ListPackageArgs listPackageArgs) + { + writer.WritePropertyName(ProjectsProperty); + writer.WriteStartArray(); + + foreach (ListPackageProjectModel reportProject in reportProjects) + { + writer.WriteStartObject(); + + writer.WritePropertyName(PathProperty); + writer.WriteValue(PathUtility.GetPathWithForwardSlashes(reportProject.ProjectPath)); + + if (reportProject.TargetFrameworkPackages?.Count > 0) + { + writer.WritePropertyName(FrameworksProperty); + + WriteFrameworkPackage(writer, reportProject.TargetFrameworkPackages, listPackageArgs); + + } + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + private static void WriteFrameworkPackage(JsonWriter writer, List reportFrameworkPackages, ListPackageArgs listPackageArgs) + { + writer.WriteStartArray(); + + foreach (ListPackageReportFrameworkPackage reportFrameworkPackage in reportFrameworkPackages) + { + writer.WriteStartObject(); + writer.WritePropertyName(FrameworkProperty); + writer.WriteValue(reportFrameworkPackage.Framework); + WriteTopLevelPackages(writer, TopLevelPackagesProperty, reportFrameworkPackage.TopLevelPackages, listPackageArgs); + WriteTransitivePackages(writer, TransitivePackagesProperty, reportFrameworkPackage.TransitivePackages, listPackageArgs); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + private static void WriteTopLevelPackages(JsonWriter writer, string property, List topLevelPackages, ListPackageArgs listPackageArgs) + { + if (topLevelPackages != null) + { + writer.WritePropertyName(property); + + writer.WriteStartArray(); + foreach (ListReportPackage topLevelPackage in topLevelPackages) + { + writer.WriteStartObject(); + writer.WritePropertyName(IdProperty); + writer.WriteValue(topLevelPackage.PackageId); + + writer.WritePropertyName(RequestedVersionProperty); + writer.WriteValue(topLevelPackage.RequestedVersion); + + writer.WritePropertyName(ResolvedVersionProperty); + writer.WriteValue(topLevelPackage.ResolvedVersion); + + if (topLevelPackage.AutoReference) + { + writer.WritePropertyName(AutoReferencedProperty); + writer.WriteValue("true"); + } + + switch (listPackageArgs.ReportType) + { + case ReportType.Outdated: + writer.WritePropertyName(LatestVersionProperty); + writer.WriteValue(topLevelPackage.LatestVersion); + break; + case ReportType.Deprecated: + WriteDeprecations(writer, topLevelPackage); + break; + case ReportType.Vulnerable: + WriteVulnerabilities(writer, topLevelPackage.Vulnerabilities); + break; + default: + break; + } + + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + } + + private static void WriteTransitivePackages(JsonWriter writer, string property, List transitivePackages, ListPackageArgs listPackageArgs) + { + if (!listPackageArgs.IncludeTransitive) + { + return; + } + + if (transitivePackages?.Count > 0) + { + writer.WritePropertyName(property); + + writer.WriteStartArray(); + foreach (ListReportPackage transitivePackage in transitivePackages) + { + writer.WriteStartObject(); + writer.WritePropertyName(IdProperty); + writer.WriteValue(transitivePackage.PackageId); + + writer.WritePropertyName(ResolvedVersionProperty); + writer.WriteValue(transitivePackage.ResolvedVersion); + + switch (listPackageArgs.ReportType) + { + case ReportType.Outdated: + writer.WritePropertyName(LatestVersionProperty); + writer.WriteValue(transitivePackage.LatestVersion); + break; + case ReportType.Deprecated: + WriteDeprecations(writer, transitivePackage); + break; + case ReportType.Vulnerable: + WriteVulnerabilities(writer, transitivePackage.Vulnerabilities); + break; + default: + break; + } + + + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + } + + private static void WriteDeprecations(JsonWriter writer, ListReportPackage listPackage) + { + if (listPackage.DeprecationReasons != null) + { + writer.WritePropertyName(DeprecationReasonsProperty); + writer.WriteStartArray(); + + foreach (var deprecationReason in listPackage.DeprecationReasons?.Reasons) + { + writer.WriteValue(deprecationReason); + } + + writer.WriteEndArray(); + } + + if (listPackage.AlternativePackage != null) + { + writer.WritePropertyName(AlternativePackageProperty); + writer.WriteStartObject(); + writer.WritePropertyName(IdProperty); + writer.WriteValue(listPackage.AlternativePackage.PackageId); + writer.WritePropertyName(VersionRangeProperty); + + var versionRangeString = VersionRangeFormatter.Instance.Format( + "p", + listPackage.AlternativePackage.Range, + VersionRangeFormatter.Instance); + + writer.WriteValue(versionRangeString); + writer.WriteEndObject(); + } + } + + private static void WriteVulnerabilities(JsonWriter writer, List vulnerabilities) + { + if (vulnerabilities == null) + { + return; + } + + writer.WritePropertyName(VulnerabilitiesProperty); + writer.WriteStartArray(); + + foreach (PackageVulnerabilityMetadata vulnerability in vulnerabilities) + { + writer.WriteStartObject(); + var severity = (vulnerability?.Severity ?? -1) switch + { + 0 => "Low", + 1 => "Moderate", + 2 => "High", + 3 => "Critical", + _ => string.Empty, + }; + + writer.WritePropertyName(SeverityProperty); + writer.WriteValue(severity); + + writer.WritePropertyName(AdvisoryUrlProperty); + writer.WriteValue(vulnerability.AdvisoryUrl); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageProjectModel.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageProjectModel.cs new file mode 100644 index 00000000000..ee4db9d3be2 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageProjectModel.cs @@ -0,0 +1,35 @@ +// 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.Collections.Generic; + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + /// + /// Calculated project data model for list report + /// + internal class ListPackageProjectModel + { + internal List ProjectProblems { get; } = new(); + internal string ProjectPath { get; private set; } + // Calculated project model data for each targetframeworks + internal List TargetFrameworkPackages { get; set; } + internal string ProjectName { get; private set; } + internal bool AutoReferenceFound { get; set; } + + public ListPackageProjectModel(string projectPath, string projectName) + { + ProjectPath = projectPath; + ProjectName = projectName; + } + + // For testing purposes only + internal ListPackageProjectModel(string projectPath) + : this(projectPath, null) { } + + internal void AddProjectInformation(ProblemType problemType, string message) + { + ProjectProblems.Add(new ReportProblem(project: ProjectPath, text: message, problemType: problemType)); + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportFrameworkPackage.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportFrameworkPackage.cs new file mode 100644 index 00000000000..1f0f4fb00c5 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportFrameworkPackage.cs @@ -0,0 +1,22 @@ +// 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.Collections.Generic; +using NuGet.Protocol; + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + /// + /// Calculated project model data for a targetframework + /// + internal class ListPackageReportFrameworkPackage + { + internal string Framework { get; set; } + internal List TopLevelPackages { get; set; } + internal List TransitivePackages { get; set; } + public ListPackageReportFrameworkPackage(string frameWork) + { + Framework = frameWork; + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportModel.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportModel.cs new file mode 100644 index 00000000000..a0b55f6b0fa --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListPackageReportModel.cs @@ -0,0 +1,33 @@ +// 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.Collections.Generic; + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + /// + /// Calculated solution/projects data model for list report + /// + internal class ListPackageReportModel + { + internal ListPackageArgs ListPackageArgs { get; } + internal List Projects { get; } = new(); + internal MSBuildAPIUtility MSBuildAPIUtility { get; } + + private ListPackageReportModel() + { } + + internal ListPackageReportModel(ListPackageArgs listPackageArgs) + { + ListPackageArgs = listPackageArgs; + MSBuildAPIUtility = new MSBuildAPIUtility(listPackageArgs.Logger); + } + + internal ListPackageProjectModel CreateProjectReportData(string projectPath, string projectName) + { + var projectModel = new ListPackageProjectModel(projectPath, projectName); + Projects.Add(projectModel); + return projectModel; + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListReportPackage.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListReportPackage.cs new file mode 100644 index 00000000000..95d42e292db --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ListReportPackage.cs @@ -0,0 +1,97 @@ +// 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.Collections.Generic; +using NuGet.Protocol; + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + internal class ListReportPackage + { + internal string PackageId { get; private set; } + internal string ResolvedVersion { get; private set; } + internal string LatestVersion { get; private set; } + public List Vulnerabilities { get; private set; } + internal PackageDeprecationMetadata DeprecationReasons { get; private set; } + internal AlternatePackageMetadata AlternativePackage { get; private set; } + internal string RequestedVersion { get; private set; } // not needed for transitive package + internal bool AutoReference { get; private set; } // not needed for transitive package + + public ListReportPackage(string packageId, string resolvedVersion, string latestVersion, List vulnerabilities, PackageDeprecationMetadata deprecationReasons, AlternatePackageMetadata alternativePackage, string requestedVersion, bool autoReference) + { + PackageId = packageId; + ResolvedVersion = resolvedVersion; + LatestVersion = latestVersion; + Vulnerabilities = vulnerabilities; + DeprecationReasons = deprecationReasons; + AlternativePackage = alternativePackage; + RequestedVersion = requestedVersion; + AutoReference = autoReference; + } + + public ListReportPackage(string packageId, string requestedVersion, string resolvedVersion, string latestVersion) + : this( + packageId: packageId, + resolvedVersion: resolvedVersion, + latestVersion: latestVersion, + vulnerabilities: null, + deprecationReasons: null, + alternativePackage: null, + requestedVersion: requestedVersion, + autoReference: false) + { } + + public ListReportPackage(string packageId, string requestedVersion, string resolvedVersion) + : this( + packageId: packageId, + requestedVersion: requestedVersion, + resolvedVersion: resolvedVersion, + autoReference: false) + { } + + public ListReportPackage(string packageId, string requestedVersion, string resolvedVersion, bool autoReference) + : this( + packageId: packageId, + requestedVersion: requestedVersion, + resolvedVersion: resolvedVersion, + latestVersion: null, + autoReference: autoReference) + { } + + public ListReportPackage(string packageId, string requestedVersion, string resolvedVersion, string latestVersion, bool autoReference) + : this( + packageId: packageId, + resolvedVersion: resolvedVersion, + latestVersion: latestVersion, + vulnerabilities: null, + deprecationReasons: null, + alternativePackage: null, + requestedVersion: requestedVersion, + autoReference: autoReference) + { } + + public ListReportPackage(string packageId, string requestedVersion, string resolvedVersion, PackageDeprecationMetadata deprecationReasons, AlternatePackageMetadata alternativePackage) + : this( + packageId: packageId, + resolvedVersion: resolvedVersion, + latestVersion: null, + vulnerabilities: null, + deprecationReasons: deprecationReasons, + alternativePackage: alternativePackage, + requestedVersion: requestedVersion, + autoReference: false) + { } + + public ListReportPackage(string packageId, string requestedVersion, string resolvedVersion, string latestVersion, List vulnerabilities) + : this( + packageId: packageId, + resolvedVersion: resolvedVersion, + latestVersion: latestVersion, + vulnerabilities: vulnerabilities, + deprecationReasons: null, + alternativePackage: null, + requestedVersion: requestedVersion, + autoReference: false) + { } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ProblemType.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ProblemType.cs new file mode 100644 index 00000000000..be33eacb5c0 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ProblemType.cs @@ -0,0 +1,11 @@ +// 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. + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + internal enum ProblemType + { + Warning, + Error // Any report problem with this type make application to return 1 instead of 0, for example if asset file is missing for 1 of the projects. + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ReportOutputFormat.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ReportOutputFormat.cs new file mode 100644 index 00000000000..fb96ed51d4c --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ReportOutputFormat.cs @@ -0,0 +1,11 @@ +// 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. + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + internal enum ReportOutputFormat + { + Console, + Json + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ReportProblem.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ReportProblem.cs new file mode 100644 index 00000000000..87deb3e66b5 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/ListPackage/ReportProblem.cs @@ -0,0 +1,25 @@ +// 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. + +namespace NuGet.CommandLine.XPlat.ListPackage +{ + /// + /// Report problem text with problem type for a project + /// + internal class ReportProblem + { + internal string Project { get; private set; } + internal string Text { get; private set; } + internal ProblemType ProblemType { get; } + + private ReportProblem() + { } + + public ReportProblem(ProblemType problemType, string project, string text) + { + ProblemType = problemType; + Project = project; + Text = text; + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 682ef6c4b47..cc9884f08bd 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -699,6 +699,24 @@ internal static string ListPkg_InvalidOptions { } } + /// + /// Looks up a localized string similar to Invalid value {0} provided for output format. The accepted values are {1}.. + /// + internal static string ListPkg_InvalidOutputFormat { + get { + return ResourceManager.GetString("ListPkg_InvalidOutputFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unsupported output format version {0} was requested. The accepted format version value is {1}.. + /// + internal static string ListPkg_InvalidOutputVersion { + get { + return ResourceManager.GetString("ListPkg_InvalidOutputVersion", resourceCulture); + } + } + /// /// Looks up a localized string similar to Latest. /// @@ -798,6 +816,24 @@ internal static string ListPkg_OutdatedDescription { } } + /// + /// Looks up a localized string similar to Set the report output format.. + /// + internal static string ListPkg_OutputFormatDescription { + get { + return ResourceManager.GetString("ListPkg_OutputFormatDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The version of report output.. + /// + internal static string ListPkg_OutputVersionDescription { + get { + return ResourceManager.GetString("ListPkg_OutputVersionDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to A path to a project, solution file or directory.. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 0a6054cefda..38dd2460b6c 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -765,4 +765,18 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT PackageReference for package '{0}' added to '{1}' and PackageVersion added to central package management file '{2}'. 0 - The package ID. 1 - Directory.Packages.props file path. 2 - Project file path. + + Set the report output format. + + + The version of report output. + + + Invalid value {0} provided for output format. The accepted values are {1}. + {0} - argument name in format option {1} - the possible values of the argument. + + + Unsupported output format version {0} was requested. The accepted format version value is {1}. + {0} - argument name in version option {1} - the possible value of the argument. + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index 769aa6939b7..9c9ddc92663 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -609,8 +609,7 @@ internal IEnumerable GetResolvedVersions( } catch (Exception) { - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingAssetsFile, assetsFile.Path)); - return null; + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingAssetsFile, assetsFile.Path)); } //The packages for the framework that were retrieved with GetRequestedVersions @@ -642,8 +641,7 @@ internal IEnumerable GetResolvedVersions( } catch (Exception) { - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingReferenceFromProject, projectPath)); - return null; + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingReferenceFromProject, projectPath)); } } else diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/ProjectPackagesPrintUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/ProjectPackagesPrintUtility.cs index 665b0808755..7065813fcc4 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/ProjectPackagesPrintUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/ProjectPackagesPrintUtility.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Threading.Tasks; -using NuGet.Configuration; +using NuGet.CommandLine.XPlat.ListPackage; using NuGet.Protocol; using NuGet.Versioning; @@ -18,95 +16,78 @@ namespace NuGet.CommandLine.XPlat.Utility internal static class ProjectPackagesPrintUtility { /// - /// A function that prints all the package references of a project + /// Returns the metadata for list package report /// /// A list of framework packages. Check - /// The project name /// Command line options /// At least one discovered package is autoreference - internal static void PrintPackages( - IEnumerable packages, string projectName, ListPackageArgs listPackageArgs, ref bool hasAutoReference) + /// The list of package metadata + internal static List GetPackagesMetadata( + IEnumerable packages, + ListPackageArgs listPackageArgs, + ref bool hasAutoReference) { - switch (listPackageArgs.ReportType) - { - case ReportType.Outdated: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ProjectUpdatesHeaderLog, projectName)); - break; - case ReportType.Deprecated: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ProjectDeprecationsHeaderLog, projectName)); - break; - case ReportType.Vulnerable: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ProjectVulnerabilitiesHeaderLog, projectName)); - break; - case ReportType.Default: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ProjectHeaderLog, projectName)); - break; - } + var projectFrameworkPackages = new List(); hasAutoReference = false; - foreach (var frameworkPackages in packages) + foreach (FrameworkPackages frameworkPackages in packages) { + string frameWork = frameworkPackages.Framework; + ListPackageReportFrameworkPackage targetFrameworkPackageMetadata = new ListPackageReportFrameworkPackage(frameWork); + projectFrameworkPackages.Add(targetFrameworkPackageMetadata); var frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; var frameworkTransitivePackages = frameworkPackages.TransitivePackages; // If no packages exist for this framework, print the // appropriate message - if (!frameworkTopLevelPackages.Any() && !frameworkTransitivePackages.Any()) + var tableHasAutoReference = false; + // Print top-level packages + if (frameworkTopLevelPackages.Any()) { - Console.ForegroundColor = ConsoleColor.Blue; - - switch (listPackageArgs.ReportType) - { - case ReportType.Outdated: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: " + Strings.ListPkg_NoUpdatesForFramework, frameworkPackages.Framework)); - break; - case ReportType.Deprecated: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: " + Strings.ListPkg_NoDeprecationsForFramework, frameworkPackages.Framework)); - break; - case ReportType.Vulnerable: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: " + Strings.ListPkg_NoVulnerabilitiesForFramework, frameworkPackages.Framework)); - break; - case ReportType.Default: - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: " + Strings.ListPkg_NoPackagesForFramework, frameworkPackages.Framework)); - break; - } - - Console.ResetColor(); + + targetFrameworkPackageMetadata.TopLevelPackages = GetFrameworkPackageMetadata( + frameworkTopLevelPackages, printingTransitive: false, listPackageArgs.ReportType, ref tableHasAutoReference).ToList(); + hasAutoReference = hasAutoReference || tableHasAutoReference; } - else + + // Print transitive packages + if (listPackageArgs.IncludeTransitive && frameworkTransitivePackages.Any()) { - // Print name of the framework - Console.ForegroundColor = ConsoleColor.Blue; - Console.WriteLine(string.Format(CultureInfo.CurrentCulture, " [{0}]: ", frameworkPackages.Framework)); - Console.ResetColor(); - - // Print top-level packages - if (frameworkTopLevelPackages.Any()) - { - var tableHasAutoReference = false; - var tableToPrint = BuildPackagesTable( - frameworkTopLevelPackages, printingTransitive: false, listPackageArgs, ref tableHasAutoReference); - if (tableToPrint != null) - { - PrintPackagesTable(tableToPrint); - hasAutoReference = hasAutoReference || tableHasAutoReference; - } - } - - // Print transitive packages - if (listPackageArgs.IncludeTransitive && frameworkTransitivePackages.Any()) - { - var tableHasAutoReference = false; - var tableToPrint = BuildPackagesTable( - frameworkTransitivePackages, printingTransitive: true, listPackageArgs, ref tableHasAutoReference); - if (tableToPrint != null) - { - PrintPackagesTable(tableToPrint); - hasAutoReference = hasAutoReference || tableHasAutoReference; - } - } + targetFrameworkPackageMetadata.TransitivePackages = GetFrameworkPackageMetadata( + frameworkTransitivePackages, printingTransitive: true, listPackageArgs.ReportType, ref tableHasAutoReference).ToList(); } } + + return projectFrameworkPackages; + } + + internal static IEnumerable GetFrameworkPackageMetadata( + IEnumerable frameworkPackages, + bool printingTransitive, + ReportType reportType, + ref bool tableHasAutoReference) + { + if (!frameworkPackages.Any()) + { + return Enumerable.Empty(); + } + + frameworkPackages = frameworkPackages.OrderBy(p => p.Name); + + var packages = frameworkPackages.Select(p => new ListReportPackage( + packageId: p.Name, + requestedVersion: printingTransitive ? string.Empty : p.OriginalRequestedVersion, + autoReference: printingTransitive ? false : p.AutoReference, + resolvedVersion: GetPackageVersion(p), + latestVersion: reportType == ReportType.Outdated ? GetPackageVersion(p, useLatest: true) : null, + vulnerabilities: reportType == ReportType.Vulnerable ? p.ResolvedPackageMetadata.Vulnerabilities?.ToList() : null, + deprecationReasons: reportType == ReportType.Deprecated ? p.ResolvedPackageMetadata.GetDeprecationMetadataAsync().Result : null, + alternativePackage: reportType == ReportType.Deprecated ? (p.ResolvedPackageMetadata.GetDeprecationMetadataAsync().Result)?.AlternatePackage : null + )); + + tableHasAutoReference = frameworkPackages.Any(p => p.AutoReference); + + return packages; } /// @@ -118,7 +99,7 @@ internal static void PrintPackages( /// Flagged if an autoreference marker was printer /// The table as a string internal static IEnumerable BuildPackagesTable( - IEnumerable packages, + IEnumerable packages, bool printingTransitive, ListPackageArgs listPackageArgs, ref bool tableHasAutoReference) @@ -130,44 +111,41 @@ internal static IEnumerable BuildPackagesTable( return null; } - packages = packages.OrderBy(p => p.Name); - var headers = BuildTableHeaders(printingTransitive, listPackageArgs); - var valueSelectors = new List> + var valueSelectors = new List> { - p => new FormattedCell(p.Name), + p => new FormattedCell(p.PackageId), p => new FormattedCell(GetAutoReferenceMarker(p, printingTransitive, ref autoReferenceFlagged)), }; // Include "Requested" version column for top level package list if (!printingTransitive) { - valueSelectors.Add(p => new FormattedCell(p.OriginalRequestedVersion)); + valueSelectors.Add(p => new FormattedCell((p as ListReportPackage)?.RequestedVersion)); } // "Resolved" version - valueSelectors.Add(p => new FormattedCell(GetPackageVersion(p))); + valueSelectors.Add(p => new FormattedCell(p.ResolvedVersion)); switch (listPackageArgs.ReportType) { case ReportType.Outdated: // "Latest" version - valueSelectors.Add(p => new FormattedCell(GetPackageVersion(p, useLatest: true))); + valueSelectors.Add(p => new FormattedCell(p.LatestVersion)); break; case ReportType.Deprecated: valueSelectors.Add(p => new FormattedCell( - PrintDeprecationReasons(p.ResolvedPackageMetadata.GetDeprecationMetadataAsync().Result))); + PrintDeprecationReasons(p.DeprecationReasons))); valueSelectors.Add(p => new FormattedCell( - PrintAlternativePackage((p.ResolvedPackageMetadata.GetDeprecationMetadataAsync().Result)?.AlternatePackage))); + PrintAlternativePackage(p.AlternativePackage))); break; case ReportType.Vulnerable: - valueSelectors.Add(p => PrintVulnerabilitiesSeverities(p.ResolvedPackageMetadata.Vulnerabilities)); - valueSelectors.Add(p => PrintVulnerabilitiesAdvisoryUrls(p.ResolvedPackageMetadata.Vulnerabilities)); + valueSelectors.Add(p => PrintVulnerabilitiesSeverities(p.Vulnerabilities)); + valueSelectors.Add(p => PrintVulnerabilitiesAdvisoryUrls(p.Vulnerabilities)); break; } - var tableToPrint = packages.ToStringTable(headers, valueSelectors.ToArray()); tableHasAutoReference = autoReferenceFlagged; @@ -220,7 +198,7 @@ private static FormattedCell VulnerabilityToSeverityFormattedCell(PackageVulnera } private static string GetAutoReferenceMarker( - InstalledPackageReference package, + ListReportPackage package, bool printingTransitive, ref bool autoReferenceFound) { @@ -229,7 +207,7 @@ private static string GetAutoReferenceMarker( return string.Empty; } - if (package.AutoReference) + if (package?.AutoReference == true) { autoReferenceFound = true; return "(A)"; @@ -325,13 +303,5 @@ internal static string[] BuildTableHeaders(bool printingTransitive, ListPackageA return result.ToArray(); } - - internal static void PrintSources(IEnumerable packageSources) - { - foreach (var source in packageSources) - { - Console.WriteLine(" " + source.Source); - } - } } } diff --git a/src/NuGet.Core/NuGet.Protocol/Properties/AssemblyInfo.cs b/src/NuGet.Core/NuGet.Protocol/Properties/AssemblyInfo.cs index d2c2bd44eef..bdb3bd7f93b 100644 --- a/src/NuGet.Core/NuGet.Protocol/Properties/AssemblyInfo.cs +++ b/src/NuGet.Core/NuGet.Protocol/Properties/AssemblyInfo.cs @@ -15,3 +15,4 @@ [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] [assembly: InternalsVisibleTo("Test.Utility, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] [assembly: InternalsVisibleTo("NuGet.PackageManagement.VisualStudio.Test, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("NuGet.XPlat.FuncTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetListPackageTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetListPackageTests.cs index 72e56c9789c..cdeff571462 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetListPackageTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetListPackageTests.cs @@ -81,7 +81,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( Assert.True(addResult.Success); var listResult = _fixture.RunDotnet(Directory.GetParent(projectA.ProjectPath).FullName, - $"list {projectA.ProjectPath} package"); + $"list {projectA.ProjectPath} package", ignoreExitCode: true); Assert.True(ContainsIgnoringSpaces(listResult.AllOutput, "No assets file was found".Replace(" ", ""))); } diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs index ef8d373b035..db5edf85cb3 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.CommandLineUtils; using Moq; @@ -97,6 +99,60 @@ public void BasicListPackageParsing_NoVerbosityOption() }); } + [Theory] + [InlineData("")] + [InlineData("--format json")] + [InlineData("--format JSON")] + [InlineData("--format json --output-version 1")] + [InlineData("--format console")] + [InlineData("--format console --output-version 1")] + public void BasicListPackage_OutputFormat_CorrectInput_Parsing_Succeeds(string outputFormatCommmand) + { + VerifyCommand( + (projectPath, mockCommandRunner, testApp, getLogLevel) => + { + // Arrange + var argList = new List() { "list" }; + + if (!string.IsNullOrEmpty(outputFormatCommmand)) + { + argList.AddRange(outputFormatCommmand.Split(' ').ToList()); + } + + argList.Add(projectPath); + + // Act + var result = testApp.Execute(argList.ToArray()); + + // Assert + mockCommandRunner.Verify(); + Assert.Equal(0, result); + }); + } + + [Theory] + [InlineData("--format xml")] + [InlineData("--format json --output-version 2")] + public void BasicListPackage_OutputFormat_BadInput_Parsing_Fails(string outputFormatCommmand) + { + VerifyCommand( + (projectPath, mockCommandRunner, testApp, getLogLevel) => + { + // Arrange + var argList = new List() { "list" }; + + if (!string.IsNullOrEmpty(outputFormatCommmand)) + { + argList.AddRange(outputFormatCommmand.Split(' ').ToList()); + } + + argList.Add(projectPath); + + // Act & Assert + Assert.Throws(() => testApp.Execute(argList.ToArray())); + }); + } + private void VerifyCommand(Action, CommandLineApplication, Func> verify) { // Arrange @@ -111,7 +167,7 @@ private void VerifyCommand(Action, Comma var mockCommandRunner = new Mock(); mockCommandRunner .Setup(m => m.ExecuteCommandAsync(It.IsAny())) - .Returns(Task.CompletedTask); + .Returns(Task.FromResult(0)); testApp.Name = "dotnet nuget_test"; ListPackageCommand.Register(testApp, @@ -130,5 +186,13 @@ private void VerifyCommand(Action, Comma } } } + + [Fact] + public void JsonRenderer_ListPackageArgse_Verify_AllFields_Covered() + { + Type listPackageArgsType = typeof(ListPackageArgs); + FieldInfo[] fields = listPackageArgsType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Assert.True(12 == fields.Length, "Number of fields are changed in ListPackageArgs.cs. Please make sure this change is accounted for GetReportParameters method in that file."); + } } } diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XplatListPackageJsonRendererTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XplatListPackageJsonRendererTests.cs new file mode 100644 index 00000000000..4d2bc9e6dbc --- /dev/null +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XplatListPackageJsonRendererTests.cs @@ -0,0 +1,1219 @@ +// 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.IO; +using System.Linq; +using System.Threading; +using FluentAssertions; +using NuGet.CommandLine.XPlat; +using NuGet.CommandLine.XPlat.ListPackage; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Configuration.Test; +using NuGet.Protocol; +using NuGet.Test.Utility; +using NuGet.Versioning; +using Xunit; + +namespace NuGet.XPlat.FuncTest +{ + [Collection("NuGet XPlat Test Collection")] + public class XplatListPackageJsonRendererTests + { + [Fact] + public void JsonRenderer_ListPackage_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Default; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork5 = "net5.0"; + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + var projectBPath = Path.Combine(pathContext.SolutionRoot, "projectB.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "A", + requestedVersion : "2.0.0", + resolvedVersion : "2.0.0") + }, + // Below transitive packages shouldn't be in json output because this report doesn't have --include-transive option. + TransitivePackages = new List() + { + new ListReportPackage( + packageId : "C", + requestedVersion : "2.0.0", + resolvedVersion : "3.1.0", + autoReference : true) + } + } + }, + projectProblems : null + ), + ( + projectBPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "B", + requestedVersion : "3.0.0", + resolvedVersion : "3.1.0") + } + }, + new ListPackageReportFrameworkPackage(frameWork5) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "B", + requestedVersion : "3.0.0", + resolvedVersion : "3.1.0") + } + } + }, + projectProblems : null + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + // Below one doesn't include any transitive packages. + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '', + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '2.0.0', + 'resolvedVersion': '2.0.0' + }} + ] + }} + ] + }}, + {{ + 'path': '{projectBPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'B', + 'requestedVersion': '3.0.0', + 'resolvedVersion': '3.1.0' + }} + ] + }}, + {{ + 'framework': 'net5.0', + 'topLevelPackages': [ + {{ + 'id': 'B', + 'requestedVersion': '3.0.0', + 'resolvedVersion': '3.1.0' + }} + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_PackageWithAutoReference_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Default; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "A", + requestedVersion : "2.0.0", + resolvedVersion : "2.0.0", + autoReference : true // this one should be detected. + ), + new ListReportPackage( + packageId : "B", + requestedVersion : "1.0.0", + resolvedVersion : "1.3.0" + ) + } + } + }, + projectProblems: null + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + // autoReferenced is set to true + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '', + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '2.0.0', + 'resolvedVersion': '2.0.0', + 'autoReferenced': 'true' + }}, + {{ + 'id': 'B', + 'requestedVersion': '1.0.0', + 'resolvedVersion': '1.3.0' + }} + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_Outdated_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Outdated; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "A", + requestedVersion : "[1.0.0,1.3.0]", + resolvedVersion : "1.0.0", + latestVersion : "2.0.0") + } + } + }, + projectProblems: null + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '--outdated', + 'sources': [ + '{pathContext.PackageSource}' + ], + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '[1.0.0,1.3.0]', + 'resolvedVersion': '1.0.0', + 'latestVersion': '2.0.0' + }} + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_Deprecated_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Deprecated; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "A", + requestedVersion : "[1.0.0,1.3.0]", + resolvedVersion : "1.0.0", + deprecationReasons : new PackageDeprecationMetadata + { + Reasons = new List() { "Other", "Legacy"}.AsEnumerable() + }, + alternativePackage : new AlternatePackageMetadata() + { + PackageId = "betterPackage", + Range = VersionRange.Parse("[*,)") + }) + } + } + }, + projectProblems: null + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '--deprecated', + 'sources': [ + '{pathContext.PackageSource}' + ], + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '[1.0.0,1.3.0]', + 'resolvedVersion': '1.0.0', + 'deprecationReasons': [ + 'Other','Legacy' + ], + 'alternativePackage': {{ + 'id': 'betterPackage', + 'versionRange': '>= 0.0.0' + }} + }} + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_Vulnerable_WithVulnerability_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Vulnerable; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "A", + requestedVersion : "[1.0.0,1.3.0]", + resolvedVersion : "1.0.0", + latestVersion : null, + vulnerabilities : new List + { + new PackageVulnerabilityMetadata() + { + Severity = 2, + AdvisoryUrl = new Uri("https://github.com/advisories/GHSA-g8j6-m4p7-5rfq") + }, + new PackageVulnerabilityMetadata() + { + Severity = 1, + AdvisoryUrl = new Uri("https://github.com/advisories/GHSA-v76m-f5cx-8rg4") + } + }) + } + } + }, + projectProblems: null + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '--vulnerable', + 'sources': [ + '{pathContext.PackageSource}' + ], + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '[1.0.0,1.3.0]', + 'resolvedVersion': '1.0.0', + 'vulnerabilities': [ + {{ + 'severity': 'High', + 'advisoryurl': 'https://github.com/advisories/GHSA-g8j6-m4p7-5rfq' + }}, + {{ + 'severity': 'Moderate', + 'advisoryurl': 'https://github.com/advisories/GHSA-v76m-f5cx-8rg4' + }} + ] + }} + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_Vulnerable_WithoutVulnerability_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Vulnerable; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { } + } + }, + new List() { } + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '--vulnerable', + 'sources': [ + '{pathContext.PackageSource}' + ], + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_IncludeTransitives_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Default; + var includeTransitive = true; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork5 = "net5.0"; + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + var projectBPath = Path.Combine(pathContext.SolutionRoot, "projectB.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: includeTransitive, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "A", + requestedVersion : "2.0.0", + resolvedVersion : "2.0.0") + }, + // Below transitive packages should be in json output because this report has --include-transive option. + TransitivePackages = new List() + { + new ListReportPackage( + packageId : "C", + requestedVersion : "2.0.0", // This is ignored for Transitive packages + resolvedVersion : "3.1.0", + autoReference : true // This is ignored for Transitive packages + ) + } + } + }, + projectProblems: null + ), + ( + projectBPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "B", + requestedVersion : "3.0.0", + resolvedVersion : "3.1.0") + } + }, + new ListPackageReportFrameworkPackage(frameWork5) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "B", + requestedVersion : "3.0.0", + resolvedVersion : "3.1.0") + }, + TransitivePackages = new List() + { + new ListReportPackage( + packageId : "D", + requestedVersion : "1.0.0", // This is ignored for Transitive packages + resolvedVersion : "1.1.0", + autoReference : true // This is ignored for Transitive packages + ) + } + } + }, + projectProblems: null + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + // Below one doesn't include any transitive packages. + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '--include-transitive', + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '2.0.0', + 'resolvedVersion': '2.0.0' + }} + ], + 'transitivePackages': [ + {{ + 'id': 'C', + 'resolvedVersion': '3.1.0' + }} + ] + }} + ] + }}, + {{ + 'path': '{projectBPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'B', + 'requestedVersion': '3.0.0', + 'resolvedVersion': '3.1.0' + }} + ] + }}, + {{ + 'framework': 'net5.0', + 'topLevelPackages': [ + {{ + 'id': 'B', + 'requestedVersion': '3.0.0', + 'resolvedVersion': '3.1.0' + }} + ], + 'transitivePackages': [ + {{ + 'id': 'D', + 'resolvedVersion': '1.1.0' + }} + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_Outdated_IncludeTransitive_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Outdated; + var includeTransitive = true; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork31 = "netcoreapp3.1"; + string frameWork5 = "net5.0"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: includeTransitive, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "A", + requestedVersion : "[1.0.0,1.3.0]", + resolvedVersion : "1.0.0", + latestVersion : "2.0.0") + } + }, + new ListPackageReportFrameworkPackage(frameWork5) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "B", + requestedVersion : "[1.0.0,1.3.0]", + resolvedVersion : "1.0.0", + latestVersion : "2.0.0") + }, + TransitivePackages = new List() + { + new ListReportPackage( + packageId : "D", + requestedVersion : "1.0.0", // This is ignored for Transitive packages + resolvedVersion : "1.1.0", + latestVersion : "3.1.0", + autoReference : true // This is ignored for Transitive packages + ) + } + } + }, + projectProblems: null + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + // Transitive packages have `latestVersion` property. + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '--outdated --include-transitive', + 'sources': [ + '{pathContext.PackageSource}' + ], + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '[1.0.0,1.3.0]', + 'resolvedVersion': '1.0.0', + 'latestVersion': '2.0.0' + }} + ] + }}, + {{ + 'framework': 'net5.0', + 'topLevelPackages': [ + {{ + 'id': 'B', + 'requestedVersion': '[1.0.0,1.3.0]', + 'resolvedVersion': '1.0.0', + 'latestVersion': '2.0.0' + }} + ], + 'transitivePackages': [ + {{ + 'id': 'D', + 'resolvedVersion': '1.1.0', + 'latestVersion': '3.1.0' + }} + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_Vulnerable_IncludeTransitive_SucceedsAsync() + { + // Arrange + var reportType = ReportType.Vulnerable; + var includeTransitive = true; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: includeTransitive, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId: "A", + requestedVersion: "[1.0.0,1.3.0]", + resolvedVersion : "1.0.0", + latestVersion: null, + vulnerabilities: new List + { + new PackageVulnerabilityMetadata() + { + Severity = 2, + AdvisoryUrl = new Uri("https://github.com/advisories/GHSA-g8j6-m4p7-5rfq") + }, + new PackageVulnerabilityMetadata() + { + Severity = 1, + AdvisoryUrl = new Uri("https://github.com/advisories/GHSA-v76m-f5cx-8rg4") + } + } + ) + }, + TransitivePackages = new List() + { + new ListReportPackage( + packageId: "D", + requestedVersion: null, + resolvedVersion: "1.1.0", + latestVersion: "3.1.0", + vulnerabilities : new List + { + new PackageVulnerabilityMetadata() + { + Severity = 3, + AdvisoryUrl = new Uri("https://github.com/advisories/GHSA-5c66-x4wm-rjfx") + } + }) + } + } + }, + projectProblems: null + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + // Vulnerabilities in transitive dependencies are detected. + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '--vulnerable --include-transitive', + 'sources': [ + '{pathContext.PackageSource}' + ], + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '[1.0.0,1.3.0]', + 'resolvedVersion': '1.0.0', + 'vulnerabilities': [ + {{ + 'severity': 'High', + 'advisoryurl': 'https://github.com/advisories/GHSA-g8j6-m4p7-5rfq' + }}, + {{ + 'severity': 'Moderate', + 'advisoryurl': 'https://github.com/advisories/GHSA-v76m-f5cx-8rg4' + }} + ] + }} + ], + 'transitivePackages': [ + {{ + 'id': 'D', + 'resolvedVersion': '1.1.0', + 'vulnerabilities': [ + {{ + 'severity': 'Critical', + 'advisoryurl': 'https://github.com/advisories/GHSA-5c66-x4wm-rjfx' + }} + ] + }} + ] + }} + ] + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + [Fact] + public void JsonRenderer_ListPackage_NoAssetFile_FailsAsync() + { + // Arrange + var reportType = ReportType.Default; + using (var pathContext = new SimpleTestPathContext()) + { + string consoleOutputFileName = Path.Combine(pathContext.SolutionRoot, "consoleOutput.txt"); + string frameWork31 = "netcoreapp3.1"; + var projectAPath = Path.Combine(pathContext.SolutionRoot, "projectA.csproj"); + var projectBPath = Path.Combine(pathContext.SolutionRoot, "projectB.csproj"); + + using (FileStream stream = new FileStream(consoleOutputFileName, FileMode.Create)) + { + using StreamWriter writer = new StreamWriter(stream); + writer.AutoFlush = true; + + ListPackageJsonRenderer jsonRenderer = new ListPackageJsonRenderer(writer); + var packageRefArgs = new ListPackageArgs( + path: pathContext.SolutionRoot, + packageSources: new List() { new PackageSource(pathContext.PackageSource) }, + frameworks: new List() { }, + reportType: reportType, + renderer: jsonRenderer, + includeTransitive: false, + prerelease: false, + highestPatch: false, + highestMinor: false, + NullLogger.Instance, + CancellationToken.None); + + ListPackageReportModel listPackageReportModel = CreateListReportModel(packageRefArgs, + ( + projectAPath, + new List() + { + new ListPackageReportFrameworkPackage(frameWork31) + { + TopLevelPackages = new List() + { + new ListReportPackage( + packageId : "A", + requestedVersion : "2.0.0", + resolvedVersion : "2.0.0") + } + } + }, + projectProblems: null + ), + ( + projectBPath, + null, + new List() { new ReportProblem(ProblemType.Error, projectBPath, $"No assets file was found for `{projectBPath}`. Please run restore before running this command.") } + ) + ); + + // Act + jsonRenderer.Render(listPackageReportModel); + } + + // Assert + // autoReferenced is set to true + var expected = SettingsTestUtils.RemoveWhitespace($@" + {{ + 'version': 1, + 'parameters': '', + 'problems': [ + {{ + 'project': '{projectBPath}', + 'level': 'error', + 'text': 'No assets file was found for `{projectBPath}`. Please run restore before running this command.' + }} + ], + 'projects': [ + {{ + 'path': '{projectAPath}', + 'frameworks': [ + {{ + 'framework': 'netcoreapp3.1', + 'topLevelPackages': [ + {{ + 'id': 'A', + 'requestedVersion': '2.0.0', + 'resolvedVersion': '2.0.0' + }} + ] + }} + ] + }}, + {{ + 'path': '{projectBPath}' + }} + ] + }} + ".Replace("'", "\"")); + + var actual = SettingsTestUtils.RemoveWhitespace(File.ReadAllText(consoleOutputFileName)); + actual.Should().Be(PathUtility.GetPathWithForwardSlashes(expected)); + } + } + + internal ListPackageReportModel CreateListReportModel(ListPackageArgs packageRefArgs, + params (string projectPath, List projectPackages, List projectProblems)[] projects) + + { + var listPackageReportModel = new ListPackageReportModel(packageRefArgs); + foreach ((string projectPath, List listPackageReportFrameworks, List projectProblems) project in projects) + { + var projectModel = new ListPackageProjectModel(project.projectPath); + projectModel.TargetFrameworkPackages = project.listPackageReportFrameworks; + + if (project.projectProblems != null) + { + foreach (var projectProblem in project.projectProblems) + { + projectModel.AddProjectInformation(projectProblem.ProblemType, projectProblem.Text); + } + } + + listPackageReportModel.Projects.Add(projectModel); + } + return listPackageReportModel; + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/ListPackageCommandRunnerTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/ListPackageCommandRunnerTests.cs index 98f052053d6..35a6df18b28 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/ListPackageCommandRunnerTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/ListPackageCommandRunnerTests.cs @@ -7,6 +7,7 @@ using System.Threading; using Moq; using NuGet.CommandLine.XPlat; +using NuGet.CommandLine.XPlat.ListPackage; using NuGet.CommandLine.XPlat.Utility; using NuGet.Common; using NuGet.Configuration; @@ -137,6 +138,7 @@ public void FiltersFrameworkPackagesCollectionWithOutdatedMetadata( var listPackageArgs = new ListPackageArgs(path: "", packageSources: Enumerable.Empty(), frameworks: Enumerable.Empty(), ReportType.Outdated, + new ListPackageConsoleRenderer(), includeTransitive: true, prerelease: false, highestPatch: false, highestMinor: false, logger: new Mock().Object, CancellationToken.None); @@ -211,6 +213,7 @@ public void FiltersFrameworkPackagesCollectionWithDeprecationMetadata( var listPackageArgs = new ListPackageArgs(path: "", packageSources: Enumerable.Empty(), frameworks: Enumerable.Empty(), ReportType.Deprecated, + new ListPackageConsoleRenderer(), includeTransitive: true, prerelease: false, highestPatch: false, highestMinor: false, logger: new Mock().Object, CancellationToken.None); @@ -284,6 +287,7 @@ public void FiltersFrameworkPackagesCollectionWithVulnerableMetadata( var listPackageArgs = new ListPackageArgs(path: "", packageSources: Enumerable.Empty(), frameworks: Enumerable.Empty(), ReportType.Vulnerable, + new ListPackageConsoleRenderer(), includeTransitive: true, prerelease: false, highestPatch: false, highestMinor: false, logger: new Mock().Object, CancellationToken.None); diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Utility/ProjectPackagesPrintUtilityTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Utility/ProjectPackagesPrintUtilityTests.cs index 92ffcdb5179..dc90a8f1f48 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Utility/ProjectPackagesPrintUtilityTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Utility/ProjectPackagesPrintUtilityTests.cs @@ -7,6 +7,7 @@ using System.Threading; using Moq; using NuGet.CommandLine.XPlat; +using NuGet.CommandLine.XPlat.ListPackage; using NuGet.CommandLine.XPlat.Utility; using NuGet.Common; using NuGet.Configuration; @@ -25,13 +26,25 @@ public void CreatesCorrectPackagesReportTableForVariousPackageCollections( object[] expectedReport) { // Arrange - if (packages is InstalledPackageReference[] packageRefs && + var installPackageRefs = packages as InstalledPackageReference[]; + bool autoReference = false; + var pkgArgs = listPackageArgs as ListPackageArgs; + var listReportPackages = installPackageRefs? + .Select(p => ProjectPackagesPrintUtility.GetFrameworkPackageMetadata( + new List { p }, + printingTransitive, + pkgArgs?.ReportType ?? ReportType.Default, + ref autoReference) + .First()) + .ToArray(); + + if (listReportPackages != null && expectedReport is FormattedCell[] expectedResult && - listPackageArgs is ListPackageArgs pkgArgs) + pkgArgs != null) { // Act var autoReferenceFound = false; - var report = ProjectPackagesPrintUtility.BuildPackagesTable(packageRefs, printingTransitive, pkgArgs, ref autoReferenceFound); + IEnumerable report = ProjectPackagesPrintUtility.BuildPackagesTable(listReportPackages, printingTransitive, pkgArgs, ref autoReferenceFound); // Assert var reportArr = report.ToArray(); @@ -100,7 +113,7 @@ expectedReport is FormattedCell[] expectedResult && }, new object[] { - new[] { OutdatedPackage, DeprecatedOutdatedPackage }, + new[] { DeprecatedOutdatedPackage, OutdatedPackage }, false, // printing transitives OutdatedReportArgs, ProcessExpectedReport(new[] @@ -144,28 +157,28 @@ expectedReport is FormattedCell[] expectedResult && private static ListPackageArgs StandardListReportArgs => StandardListReportArgsCache ?? (StandardListReportArgsCache = new ListPackageArgs( path: string.Empty, packageSources: Enumerable.Empty(), frameworks: Enumerable.Empty(), - ReportType.Default, includeTransitive: false, + ReportType.Default, new ListPackageConsoleRenderer(), includeTransitive: false, prerelease: false, highestPatch: false, highestMinor: false, logger: new Mock().Object, cancellationToken: CancellationToken.None)); private static ListPackageArgs OutdatedReportArgsCache; private static ListPackageArgs OutdatedReportArgs => OutdatedReportArgsCache ?? (OutdatedReportArgsCache = new ListPackageArgs( path: string.Empty, packageSources: Enumerable.Empty(), frameworks: Enumerable.Empty(), - ReportType.Outdated, includeTransitive: false, + ReportType.Outdated, new ListPackageConsoleRenderer(), includeTransitive: false, prerelease: false, highestPatch: false, highestMinor: false, logger: new Mock().Object, cancellationToken: CancellationToken.None)); private static ListPackageArgs DeprecatedReportArgsCache; private static ListPackageArgs DeprecatedReportArgs => DeprecatedReportArgsCache ?? (DeprecatedReportArgsCache = new ListPackageArgs( path: string.Empty, packageSources: Enumerable.Empty(), frameworks: Enumerable.Empty(), - ReportType.Deprecated, includeTransitive: false, + ReportType.Deprecated, new ListPackageConsoleRenderer(), includeTransitive: false, prerelease: false, highestPatch: false, highestMinor: false, logger: new Mock().Object, cancellationToken: CancellationToken.None)); private static ListPackageArgs VulnerableReportArgsCache; private static ListPackageArgs VulnerableReportArgs => VulnerableReportArgsCache ?? (VulnerableReportArgsCache = new ListPackageArgs( path: string.Empty, packageSources: Enumerable.Empty(), frameworks: Enumerable.Empty(), - ReportType.Vulnerable, includeTransitive: false, + ReportType.Vulnerable, new ListPackageConsoleRenderer(), includeTransitive: false, prerelease: false, highestPatch: false, highestMinor: false, logger: new Mock().Object, cancellationToken: CancellationToken.None)); private static InstalledPackageReference StandardPackageCache;