From 708f4e2770377791abcd756a6a4ce19b41f70e2b Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 27 Mar 2024 21:09:38 -0700 Subject: [PATCH 01/53] applied changes from pragnya17-dotnet-nuget-why --- .../WhyPackage/IWhyPackageCommandRunner.cs | 12 + .../WhyPackage/WhyPackageArgs.cs | 39 +++ .../WhyPackage/WhyPackageCommandRunner.cs | 262 ++++++++++++++++++ .../Commands/WhyCommand.cs | 63 +++++ .../NuGet.CommandLine.XPlat/Program.cs | 1 + .../Strings.Designer.cs | 55 ++++ .../NuGet.CommandLine.XPlat/Strings.resx | 15 + 7 files changed, 447 insertions(+) create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/IWhyPackageCommandRunner.cs create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageArgs.cs create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageCommandRunner.cs create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/IWhyPackageCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/IWhyPackageCommandRunner.cs new file mode 100644 index 00000000000..f6ae0a6da23 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/IWhyPackageCommandRunner.cs @@ -0,0 +1,12 @@ +// 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.Threading.Tasks; + +namespace NuGet.CommandLine.XPlat +{ + internal interface IWhyPackageCommandRunner + { + Task ExecuteCommandAsync(WhyPackageArgs whyPackageArgs); + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageArgs.cs new file mode 100644 index 00000000000..0889c5014db --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageArgs.cs @@ -0,0 +1,39 @@ +// 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 NuGet.Common; + +namespace NuGet.CommandLine.XPlat +{ + internal class WhyPackageArgs + { + public ILogger Logger { get; } + public string Path { get; } + public string Package { get; } + public IEnumerable Frameworks { get; } + + + /// + /// A constructor for the arguments of Why package + /// command. This is used to execute the runner's + /// method + /// + /// The path to the solution or project file + /// The package to look up the dependency paths for + /// The user inputted frameworks to look up for their packages + /// + public WhyPackageArgs( + string path, + string package, + IEnumerable frameworks, + ILogger logger) + { + Path = path ?? throw new ArgumentNullException(nameof(path)); + Package = package ?? throw new ArgumentNullException(nameof(package)); + Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageCommandRunner.cs new file mode 100644 index 00000000000..61c8e453338 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageCommandRunner.cs @@ -0,0 +1,262 @@ +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Build.Evaluation; +using NuGet.ProjectModel; + +namespace NuGet.CommandLine.XPlat +{ + internal class WhyPackageCommandRunner : IWhyPackageCommandRunner + { + private const string ProjectAssetsFile = "ProjectAssetsFile"; + private const string ProjectName = "MSBuildProjectName"; + + /// + /// Use CLI arguments to execute why command. + /// + /// CLI arguments. + /// + public Task ExecuteCommandAsync(WhyPackageArgs whyPackageArgs) + { + //TODO: figure out how to use current directory if path is not passed in + var projectsPaths = Path.GetExtension(whyPackageArgs.Path).Equals(".sln") ? + MSBuildAPIUtility.GetProjectsFromSolution(whyPackageArgs.Path).Where(f => File.Exists(f)) : + new List(new string[] { whyPackageArgs.Path }); + + // the package you want to print the dependency paths for + var package = whyPackageArgs.Package; + + var msBuild = new MSBuildAPIUtility(whyPackageArgs.Logger); + + 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 (!MSBuildAPIUtility.IsPackageReferenceProject(project)) + { + Console.Error.WriteLine(string.Format(CultureInfo.CurrentCulture, + Strings.Error_NotPRProject, + projectPath)); + Console.WriteLine(); + continue; + } + + var projectName = project.GetPropertyValue(ProjectName); + + var assetsPath = project.GetPropertyValue(ProjectAssetsFile); + + // 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 lockFileFormat = new LockFileFormat(); + var assetsFile = lockFileFormat.Read(assetsPath); + + // Assets file validation + if (assetsFile.PackageSpec != null && + assetsFile.Targets != null && + assetsFile.Targets.Count != 0) + { + + // Get all the packages that are referenced in a project + // TODO: for now, just passing in true for the last two args (hardcoded) so I need to change that + var packages = msBuild.GetResolvedVersions(project.FullPath, whyPackageArgs.Frameworks, assetsFile, true, true); + + // 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) + { + // No packages means that no package references at all were found in the current framework + if (!packages.Any()) + { + Console.WriteLine(string.Format(Strings.WhyPkg_NoPackagesFoundForFrameworks, projectName)); + } + else + { + Console.Write($"Project '{projectName}' has the following dependency graph for '{package}'\n"); + RunWhyCommand(packages, assetsFile.Targets, package); + } + } + } + else + { + Console.WriteLine(string.Format(Strings.ListPkg_ErrorReadingAssetsFile, assetsPath)); + } + + // Unload project + ProjectCollection.GlobalProjectCollection.UnloadProject(project); + } + } + + return Task.CompletedTask; + } + + /// + /// Run the why command, print out output to console. + /// + /// All packages in the project. Split up by top level packages and transitive packages. + /// All target frameworks in project and corresponding info about frameworks. + /// Package passed in as CLI argument. + private void RunWhyCommand(IEnumerable packages, IList targetFrameworks, string package) + { + foreach (var frameworkPackages in packages) + { + // Print framework name + var frameworkName = frameworkPackages.Framework; + PrintFrameworkHeader(frameworkName); + + // Get all the top level packages in the framework + var frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; + // Get all the libraries in the framework + var libraries = targetFrameworks.FirstOrDefault(i => i.Name == frameworkPackages.Framework).Libraries; + + var dependencyGraph = FindPaths(frameworkTopLevelPackages, libraries, package); + PrintDependencyGraphInFramework(dependencyGraph); + } + } + + /// + /// Find all dependency paths. + /// + /// "root nodes" of the graph. + /// All libraries in a given project. + /// The package name CLI argument. + /// + private List> FindPaths(IEnumerable topLevelPackages, IList libraries, string destination) + { + List> dependencyGraph = new List>(); + List> listOfPaths = new List>(); + HashSet visited = new HashSet(); + foreach (var package in topLevelPackages) + { + Dependency dep; + dep.name = package.Name; + dep.version = package.OriginalRequestedVersion; + + List path = new List + { + dep + }; + + var dependencyPathsInFramework = DfsTraversal(package.Name, libraries, visited, path, listOfPaths, destination); + dependencyGraph.AddRange(dependencyPathsInFramework); + } + return dependencyGraph; + } + + struct Dependency + { + public string name; + public string version; + } + + /// + /// DFS from root node until destination is found (if destination ID exists) + /// + /// Top level packahe. + /// All libraries in the target framework. + /// A set to keep track of all nodes that have been visisted. + /// Keep track of path as DFS happens. + /// List of all dependency paths that lead to destination. + /// CLI argument with the package that is passed in. + /// + private List> DfsTraversal(string rootPackage, IList libraries, HashSet visited, List path, List> listOfPaths, string destination) + { + if (rootPackage == destination) + { + // copy what is stored in list variable over to list that you allocate memory for + List pathToAdd = new List(); + foreach (var p in path) + { + pathToAdd.Add(p); + } + listOfPaths.Add(pathToAdd); + return listOfPaths; + } + + // Find the library that matches the root package's ID and get all its dependencies + LockFileTargetLibrary library = libraries.FirstOrDefault(i => i.Name == rootPackage); + var listDependencies = library.Dependencies; + + if (listDependencies.Count != 0) + { + foreach (var dependency in listDependencies) + { + Dependency dep; + dep.name = dependency.Id; + dep.version = dependency.VersionRange.MinVersion.Version.ToString(); + if (!visited.Contains(dep)) + { + visited.Add(dep); + path.Add(dep); + + // recurse + DfsTraversal(dependency.Id, libraries, visited, path, listOfPaths, destination); + + // backtrack + path.RemoveAt(path.Count - 1); + visited.Remove(dep); + } + } + } + + return listOfPaths; + } + + /// + /// Print dependency graph with syntax/punctuation. + /// + /// List of all paths that lead to destination. + private void PrintDependencyGraphInFramework(List> listOfPaths) + { + if (listOfPaths.Count == 0) + { + Console.Write("\t\t"); + Console.Write("No dependency paths found."); + } + + foreach (var path in listOfPaths) + { + Console.Write("\t\t"); + int iteration = 0; + foreach (var package in path) + { + Console.Write($"{package.name} ({package.version})"); + // don't print arrows after the last package in the path + if (iteration < path.Count - 1) + { + Console.Write(" -> "); + } + iteration++; + } + Console.Write("\n"); + } + } + + /// + /// Print framework header. + /// + /// Name of framework. + private void PrintFrameworkHeader(string frameworkName) + { + Console.Write("\t"); + Console.Write($"[{frameworkName}]"); + Console.Write(":\n"); + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs new file mode 100644 index 00000000000..355ab0c667d --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs @@ -0,0 +1,63 @@ +// 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.Globalization; +using Microsoft.Extensions.CommandLineUtils; +using NuGet.Common; + +namespace NuGet.CommandLine.XPlat +{ + internal class WhyCommand + { + public static void Register(CommandLineApplication app, Func getLogger, Func getCommandRunner) + { + app.Command("why", why => + { + why.Description = Strings.Why_Description; + why.HelpOption(XPlatUtility.HelpOption); + + CommandArgument path = why.Argument( + "", + Strings.Why_PathDescription, + multipleValues: false); + + CommandArgument package = why.Argument( + "", + Strings.WhyCommandPackageDescription, + multipleValues: false); + + CommandOption frameworks = why.Option( + "--framework", + Strings.WhyFrameworkDescription, + CommandOptionType.MultipleValue); + + why.OnExecute(async () => + { + ValidatePackage(package); + + var logger = getLogger(); + var WhyPackageArgs = new WhyPackageArgs( + path.Value, + package.Value, + frameworks.Values, + logger); + + var WhyPackageCommandRunner = getCommandRunner(); + await WhyPackageCommandRunner.ExecuteCommandAsync(WhyPackageArgs); + return 0; + }); + }); + } + + private static void ValidatePackage(CommandArgument argument) + { + if (string.IsNullOrEmpty(argument.Value)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Error_PkgMissingArgument, + "Why", + argument.Name)); + } + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index d4f389b212f..6638b83e339 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -255,6 +255,7 @@ private static CommandLineApplication InitializeApp(string[] args, CommandOutput VerifyCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new VerifyCommandRunner()); TrustedSignersCommand.Register(app, getHidePrefixLogger, setLogLevel); SignCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new SignCommandRunner()); + WhyCommand.Register(app, getHidePrefixLogger, () => new WhyPackageCommandRunner()); } app.FullName = Strings.App_FullName; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index b7fa2fd8db5..81c3edceadf 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -2213,5 +2213,60 @@ internal static string Warning_HttpServerUsage_MultipleSources { return ResourceManager.GetString("Warning_HttpServerUsage_MultipleSources", resourceCulture); } } + + /// + /// Looks up a localized string similar to Lists the dependency graph for a particular pakage for a project or solution.. + /// + internal static string Why_Description + { + get + { + return ResourceManager.GetString("Why_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A path to a project, solution file or directory.. + /// + internal static string Why_PathDescription + { + get + { + return ResourceManager.GetString("Why_PathDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A package name to look-up in the dependency graph.. + /// + internal static string WhyCommandPackageDescription + { + get + { + return ResourceManager.GetString("WhyCommandPackageDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Specifies the target framework to look-up in the dependency graph while searching for a package.. + /// + internal static string WhyFrameworkDescription + { + get + { + return ResourceManager.GetString("WhyFrameworkDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No packages were found for this framework.. + /// + internal static string WhyPkg_NoPackagesFoundForFrameworks + { + get + { + return ResourceManager.GetString("WhyPkg_NoPackagesFoundForFrameworks", resourceCulture); + } + } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 2073706b3d9..6c8c8fac657 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -912,4 +912,19 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Gets the NuGet configuration settings that will be applied. + + A package name to look-up in the dependency graph. + + + Specifies the target framework to look-up in the dependency graph while searching for a package. + + + Lists the dependency graph for a particular pakage for a project or solution. + + + A path to a project, solution file or directory. + + + No packages were found for this framework. + From 10245f8d10ad95a234c7105685e461a60303077f Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 27 Mar 2024 22:49:14 -0700 Subject: [PATCH 02/53] cleaning up some old code --- .../IWhyCommandRunner.cs} | 4 +- .../WhyCommandArgs.cs} | 15 ++-- .../WhyCommandRunner.cs} | 27 ++++--- .../Commands/WhyCommand.cs | 46 +++++++---- .../NuGet.CommandLine.XPlat/Program.cs | 2 +- .../Strings.Designer.cs | 78 ++++++++----------- .../NuGet.CommandLine.XPlat/Strings.resx | 18 +++-- 7 files changed, 99 insertions(+), 91 deletions(-) rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/{WhyPackage/IWhyPackageCommandRunner.cs => WhyCommand/IWhyCommandRunner.cs} (69%) rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/{WhyPackage/WhyPackageArgs.cs => WhyCommand/WhyCommandArgs.cs} (71%) rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/{WhyPackage/WhyPackageCommandRunner.cs => WhyCommand/WhyCommandRunner.cs} (91%) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/IWhyPackageCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs similarity index 69% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/IWhyPackageCommandRunner.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs index f6ae0a6da23..73ee83b4084 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/IWhyPackageCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs @@ -5,8 +5,8 @@ namespace NuGet.CommandLine.XPlat { - internal interface IWhyPackageCommandRunner + internal interface IWhyCommandRunner { - Task ExecuteCommandAsync(WhyPackageArgs whyPackageArgs); + Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs); } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs similarity index 71% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageArgs.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs index 0889c5014db..8a4e71968d8 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs @@ -7,27 +7,26 @@ namespace NuGet.CommandLine.XPlat { - internal class WhyPackageArgs + internal class WhyCommandArgs { public ILogger Logger { get; } public string Path { get; } public string Package { get; } - public IEnumerable Frameworks { get; } - + public List Frameworks { get; } /// - /// A constructor for the arguments of Why package + /// A constructor for the arguments of the 'why' /// command. This is used to execute the runner's /// method /// /// The path to the solution or project file - /// The package to look up the dependency paths for - /// The user inputted frameworks to look up for their packages + /// The package for which we show the dependency graph + /// The target frameworks for which we show the dependency graph /// - public WhyPackageArgs( + public WhyCommandArgs( string path, string package, - IEnumerable frameworks, + List frameworks, ILogger logger) { Path = path ?? throw new ArgumentNullException(nameof(path)); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs similarity index 91% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageCommandRunner.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 61c8e453338..0ceb6cd9562 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyPackage/WhyPackageCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -12,7 +12,7 @@ namespace NuGet.CommandLine.XPlat { - internal class WhyPackageCommandRunner : IWhyPackageCommandRunner + internal class WhyCommandRunner : IWhyCommandRunner { private const string ProjectAssetsFile = "ProjectAssetsFile"; private const string ProjectName = "MSBuildProjectName"; @@ -20,21 +20,21 @@ internal class WhyPackageCommandRunner : IWhyPackageCommandRunner /// /// Use CLI arguments to execute why command. /// - /// CLI arguments. + /// CLI arguments. /// - public Task ExecuteCommandAsync(WhyPackageArgs whyPackageArgs) + public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) { //TODO: figure out how to use current directory if path is not passed in - var projectsPaths = Path.GetExtension(whyPackageArgs.Path).Equals(".sln") ? - MSBuildAPIUtility.GetProjectsFromSolution(whyPackageArgs.Path).Where(f => File.Exists(f)) : - new List(new string[] { whyPackageArgs.Path }); + var projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") ? + MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) : + new List(new string[] { whyCommandArgs.Path }); // the package you want to print the dependency paths for - var package = whyPackageArgs.Package; + var package = whyCommandArgs.Package; - var msBuild = new MSBuildAPIUtility(whyPackageArgs.Logger); + var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); - foreach (var projectPath in projectsPaths) + foreach (var projectPath in projectPaths) { //Open project to evaluate properties for the assets //file and the name of the project @@ -73,8 +73,7 @@ public Task ExecuteCommandAsync(WhyPackageArgs whyPackageArgs) { // Get all the packages that are referenced in a project - // TODO: for now, just passing in true for the last two args (hardcoded) so I need to change that - var packages = msBuild.GetResolvedVersions(project.FullPath, whyPackageArgs.Frameworks, assetsFile, true, true); + var packages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true); // If packages equals null, it means something wrong happened // with reading the packages and it was handled and message printed @@ -84,7 +83,7 @@ public Task ExecuteCommandAsync(WhyPackageArgs whyPackageArgs) // No packages means that no package references at all were found in the current framework if (!packages.Any()) { - Console.WriteLine(string.Format(Strings.WhyPkg_NoPackagesFoundForFrameworks, projectName)); + Console.WriteLine(string.Format(Strings.WhyCommand_Error_NoPackagesFoundForFrameworks, projectName)); } else { @@ -168,9 +167,9 @@ struct Dependency /// /// DFS from root node until destination is found (if destination ID exists) /// - /// Top level packahe. + /// Top level package. /// All libraries in the target framework. - /// A set to keep track of all nodes that have been visisted. + /// A set to keep track of all nodes that have been visited. /// Keep track of path as DFS happens. /// List of all dependency paths that lead to destination. /// CLI argument with the package that is passed in. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs index 355ab0c667d..b4e4625e256 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs @@ -3,60 +3,76 @@ using System; using System.Globalization; +using System.Linq; using Microsoft.Extensions.CommandLineUtils; using NuGet.Common; +using NuGet.Frameworks; namespace NuGet.CommandLine.XPlat { internal class WhyCommand { - public static void Register(CommandLineApplication app, Func getLogger, Func getCommandRunner) + public static void Register(CommandLineApplication app, Func getLogger, Func getCommandRunner) { app.Command("why", why => { - why.Description = Strings.Why_Description; + why.Description = Strings.WhyCommand_Description; why.HelpOption(XPlatUtility.HelpOption); CommandArgument path = why.Argument( "", - Strings.Why_PathDescription, + Strings.WhyCommand_PathArgument_Description, multipleValues: false); CommandArgument package = why.Argument( "", - Strings.WhyCommandPackageDescription, + Strings.WhyCommand_PackageArgument_Description, multipleValues: false); CommandOption frameworks = why.Option( "--framework", - Strings.WhyFrameworkDescription, + Strings.WhyCommand_FrameworkArgument_Description, CommandOptionType.MultipleValue); - why.OnExecute(async () => + why.OnExecute(() => { - ValidatePackage(package); + // TODO: Can path be empty? + ValidatePackageArgument(package); + ValidateFrameworksOption(frameworks); var logger = getLogger(); - var WhyPackageArgs = new WhyPackageArgs( + var whyCommandArgs = new WhyCommandArgs( path.Value, package.Value, frameworks.Values, logger); - var WhyPackageCommandRunner = getCommandRunner(); - await WhyPackageCommandRunner.ExecuteCommandAsync(WhyPackageArgs); + var whyCommandRunner = getCommandRunner(); + whyCommandRunner.ExecuteCommandAsync(whyCommandArgs); return 0; }); }); } - private static void ValidatePackage(CommandArgument argument) + private static void ValidatePackageArgument(CommandArgument package) { - if (string.IsNullOrEmpty(argument.Value)) + if (string.IsNullOrEmpty(package.Value)) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Error_PkgMissingArgument, - "Why", - argument.Name)); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.WhyCommand_Error_ArgumentCannotBeEmpty, package.Name)); + } + } + + private static void ValidateFrameworksOption(CommandOption framework) + { + var frameworks = framework.Values.Select(f => + NuGetFramework.Parse( + f.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .ToArray()[0])); + + if (frameworks.Any(f => f.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(Strings.ListPkg_InvalidFramework, nameof(framework)); } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index 6638b83e339..8adf56f2696 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -255,7 +255,7 @@ private static CommandLineApplication InitializeApp(string[] args, CommandOutput VerifyCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new VerifyCommandRunner()); TrustedSignersCommand.Register(app, getHidePrefixLogger, setLogLevel); SignCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new SignCommandRunner()); - WhyCommand.Register(app, getHidePrefixLogger, () => new WhyPackageCommandRunner()); + WhyCommand.Register(app, getHidePrefixLogger, () => new WhyCommandRunner()); } app.FullName = Strings.App_FullName; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 81c3edceadf..6cac3553d60 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -564,15 +564,6 @@ internal static string Error_CentralPackageVersions_VersionsNotAllowed { } } - /// - /// Looks up a localized string similar to {0} is not a valid integer.. - /// - internal static string Error_invalid_number { - get { - return ResourceManager.GetString("Error_invalid_number", resourceCulture); - } - } - /// /// Looks up a localized string similar to '{0}' is not a valid config key in config section.. /// @@ -2213,59 +2204,58 @@ internal static string Warning_HttpServerUsage_MultipleSources { return ResourceManager.GetString("Warning_HttpServerUsage_MultipleSources", resourceCulture); } } - + /// - /// Looks up a localized string similar to Lists the dependency graph for a particular pakage for a project or solution.. + /// Looks up a localized string similar to Lists the dependency graph for a particular package for a given project or solution.. /// - internal static string Why_Description - { - get - { - return ResourceManager.GetString("Why_Description", resourceCulture); + internal static string WhyCommand_Description { + get { + return ResourceManager.GetString("WhyCommand_Description", resourceCulture); } } - + /// - /// Looks up a localized string similar to A path to a project, solution file or directory.. + /// Looks up a localized string similar to Unable to run 'dotnet nuget why'. The '{0}' argument cannot be empty.. /// - internal static string Why_PathDescription - { - get - { - return ResourceManager.GetString("Why_PathDescription", resourceCulture); + internal static string WhyCommand_Error_ArgumentCannotBeEmpty { + get { + return ResourceManager.GetString("WhyCommand_Error_ArgumentCannotBeEmpty", resourceCulture); } } - + /// - /// Looks up a localized string similar to A package name to look-up in the dependency graph.. + /// Looks up a localized string similar to No packages were found for this framework.. /// - internal static string WhyCommandPackageDescription - { - get - { - return ResourceManager.GetString("WhyCommandPackageDescription", resourceCulture); + internal static string WhyCommand_Error_NoPackagesFoundForFrameworks { + get { + return ResourceManager.GetString("WhyCommand_Error_NoPackagesFoundForFrameworks", resourceCulture); } } - + /// /// Looks up a localized string similar to Specifies the target framework to look-up in the dependency graph while searching for a package.. /// - internal static string WhyFrameworkDescription - { - get - { - return ResourceManager.GetString("WhyFrameworkDescription", resourceCulture); + internal static string WhyCommand_FrameworkArgument_Description { + get { + return ResourceManager.GetString("WhyCommand_FrameworkArgument_Description", resourceCulture); } } - + /// - /// Looks up a localized string similar to No packages were found for this framework.. + /// Looks up a localized string similar to A package name to look-up in the dependency graph.. /// - internal static string WhyPkg_NoPackagesFoundForFrameworks - { - get - { - return ResourceManager.GetString("WhyPkg_NoPackagesFoundForFrameworks", resourceCulture); + internal static string WhyCommand_PackageArgument_Description { + get { + return ResourceManager.GetString("WhyCommand_PackageArgument_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A path to a project, solution file or directory.. + /// + internal static string WhyCommand_PathArgument_Description { + get { + return ResourceManager.GetString("WhyCommand_PathArgument_Description", resourceCulture); } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 6c8c8fac657..7fe15306186 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -912,19 +912,23 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Gets the NuGet configuration settings that will be applied. - + A package name to look-up in the dependency graph. - + Specifies the target framework to look-up in the dependency graph while searching for a package. - - Lists the dependency graph for a particular pakage for a project or solution. + + Lists the dependency graph for a particular package for a given project or solution. - + A path to a project, solution file or directory. - + No packages were found for this framework. - + + Unable to run 'dotnet nuget why'. The '{0}' argument cannot be empty. + {0} - Argument which was not provided + + \ No newline at end of file From 2098b798d2a40a1ca07c00296957b8919342b5dd Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Tue, 2 Apr 2024 21:39:30 -0700 Subject: [PATCH 03/53] wipping --- .../WhyCommand/WhyCommandRunner.cs | 193 +++++++++++++----- .../Commands/WhyCommand.cs | 2 +- .../NuGet.CommandLine.XPlat.csproj | 4 +- .../NuGet.CommandLine.XPlat/Program.cs | 4 +- 4 files changed, 147 insertions(+), 56 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 0ceb6cd9562..c48650a6678 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.Build.Evaluation; using NuGet.ProjectModel; @@ -17,6 +18,8 @@ internal class WhyCommandRunner : IWhyCommandRunner private const string ProjectAssetsFile = "ProjectAssetsFile"; private const string ProjectName = "MSBuildProjectName"; + private const string LeafSymbol = "└──"; + /// /// Use CLI arguments to execute why command. /// @@ -24,21 +27,21 @@ internal class WhyCommandRunner : IWhyCommandRunner /// public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) { - //TODO: figure out how to use current directory if path is not passed in - var projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") ? - MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) : - new List(new string[] { whyCommandArgs.Path }); + // TODO: figure out how to use current directory if path is not passed in + var projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") + ? MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) + : new List() { whyCommandArgs.Path }; // the package you want to print the dependency paths for - var package = whyCommandArgs.Package; + string package = whyCommandArgs.Package; var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); foreach (var projectPath in projectPaths) { - //Open project to evaluate properties for the assets - //file and the name of the project - var project = MSBuildAPIUtility.GetProject(projectPath); + // Open project to evaluate properties for the assets + // file and the name of the project + Project project = MSBuildAPIUtility.GetProject(projectPath); if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) { @@ -49,9 +52,8 @@ public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) continue; } - var projectName = project.GetPropertyValue(ProjectName); - - var assetsPath = project.GetPropertyValue(ProjectAssetsFile); + string projectName = project.GetPropertyValue(ProjectName); + string assetsPath = project.GetPropertyValue(ProjectAssetsFile); // If the file was not found, print an error message and continue to next project if (!File.Exists(assetsPath)) @@ -64,7 +66,7 @@ public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) else { var lockFileFormat = new LockFileFormat(); - var assetsFile = lockFileFormat.Read(assetsPath); + LockFile assetsFile = lockFileFormat.Read(assetsPath); // Assets file validation if (assetsFile.PackageSpec != null && @@ -73,7 +75,7 @@ public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) { // Get all the packages that are referenced in a project - var packages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true); + List packages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true); // If packages equals null, it means something wrong happened // with reading the packages and it was handled and message printed @@ -88,7 +90,7 @@ public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) else { Console.Write($"Project '{projectName}' has the following dependency graph for '{package}'\n"); - RunWhyCommand(packages, assetsFile.Targets, package); + FindDependencyGraphsForPackage(packages, assetsFile.Targets, package); } } } @@ -111,21 +113,25 @@ public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) /// All packages in the project. Split up by top level packages and transitive packages. /// All target frameworks in project and corresponding info about frameworks. /// Package passed in as CLI argument. - private void RunWhyCommand(IEnumerable packages, IList targetFrameworks, string package) + private void FindDependencyGraphsForPackage(IEnumerable packages, IList targetFrameworks, string package) { foreach (var frameworkPackages in packages) { // Print framework name - var frameworkName = frameworkPackages.Framework; - PrintFrameworkHeader(frameworkName); + string frameworkName = frameworkPackages.Framework; + Console.Write($"\n\t[{frameworkName}]:\n\n"); // Get all the top level packages in the framework - var frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; + IEnumerable frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; + // Get all the libraries in the framework - var libraries = targetFrameworks.FirstOrDefault(i => i.Name == frameworkPackages.Framework).Libraries; + IList libraries = targetFrameworks + .FirstOrDefault(i => i.Name == frameworkPackages.Framework) + .Libraries; - var dependencyGraph = FindPaths(frameworkTopLevelPackages, libraries, package); - PrintDependencyGraphInFramework(dependencyGraph); + List> dependencyGraph = GetDependencyGraphPerFramework(frameworkTopLevelPackages, libraries, package); + //PrintDependencyGraphPerFramework(dependencyGraph); + AlternativePrintDependencyPaths(dependencyGraph); // TODO: this has duplicates, fix it } } @@ -136,34 +142,29 @@ private void RunWhyCommand(IEnumerable packages, IListAll libraries in a given project. /// The package name CLI argument. /// - private List> FindPaths(IEnumerable topLevelPackages, IList libraries, string destination) + private List> GetDependencyGraphPerFramework(IEnumerable topLevelPackages, IList libraries, string destination) { - List> dependencyGraph = new List>(); - List> listOfPaths = new List>(); - HashSet visited = new HashSet(); + var dependencyGraph = new List>(); + var listOfPaths = new List>(); + var visited = new HashSet(); + foreach (var package in topLevelPackages) { - Dependency dep; - dep.name = package.Name; - dep.version = package.OriginalRequestedVersion; - List path = new List { - dep + new Dependency() + { + Name = package.Name, + Version = package.OriginalRequestedVersion + } }; - var dependencyPathsInFramework = DfsTraversal(package.Name, libraries, visited, path, listOfPaths, destination); + List> dependencyPathsInFramework = DfsTraversal(package.Name, libraries, visited, path, listOfPaths, destination); dependencyGraph.AddRange(dependencyPathsInFramework); } return dependencyGraph; } - struct Dependency - { - public string name; - public string version; - } - /// /// DFS from root node until destination is found (if destination ID exists) /// @@ -197,8 +198,8 @@ private List> DfsTraversal(string rootPackage, IList> DfsTraversal(string rootPackage, IList /// List of all paths that lead to destination. - private void PrintDependencyGraphInFramework(List> listOfPaths) + private void PrintDependencyGraphPerFramework(List> listOfPaths) { if (listOfPaths.Count == 0) { - Console.Write("\t\t"); - Console.Write("No dependency paths found."); + Console.Write("\t\tNo dependency paths found.\n"); } foreach (var path in listOfPaths) @@ -235,7 +235,7 @@ private void PrintDependencyGraphInFramework(List> listOfPaths) int iteration = 0; foreach (var package in path) { - Console.Write($"{package.name} ({package.version})"); + Console.Write($"{package.Name} ({package.Version})"); // don't print arrows after the last package in the path if (iteration < path.Count - 1) { @@ -247,15 +247,106 @@ private void PrintDependencyGraphInFramework(List> listOfPaths) } } - /// - /// Print framework header. - /// - /// Name of framework. - private void PrintFrameworkHeader(string frameworkName) + private void AlternativePrintDependencyPaths(List> listOfPaths) { - Console.Write("\t"); - Console.Write($"[{frameworkName}]"); - Console.Write(":\n"); + if (listOfPaths.Count == 0) + { + Console.Write("\t\tNo dependency paths found.\n"); + } + + Console.OutputEncoding = System.Text.Encoding.UTF8; // Set UTF-8 encoding + + foreach (var path in listOfPaths) + { + int iteration = 0; + var dependencyPathOutput = new StringBuilder(); + + foreach (var package in path) + { + if (iteration == 0) + { + Console.Write($"{new string(' ', 12)}"); + + dependencyPathOutput.Append($"\t"); + } + else + { + string indentation = new string(' ', iteration * 4); + + Console.Write($"\t{indentation}{LeafSymbol} "); + dependencyPathOutput.Append($"\t{indentation}{LeafSymbol} "); + } + + Console.Write($"{package.Name} ({package.Version})\n"); + dependencyPathOutput.Append($"{package.Name} ({package.Version})\n"); + + iteration++; + } + + Console.Write("\n"); + } } + + struct Dependency + { + public string Name; + public string Version; + } + + private List> NonRecursiveDfsTraversal(string rootPackage, IList libraries, string destination) + { + var dependencyPaths = new List>(); + var stack = new Stack<(string package, List path)>(); + var visited = new HashSet(); + + stack.Push((rootPackage, new List + { + new Dependency + { + Name = rootPackage, + Version = GetPackageVersion(rootPackage, libraries) + } + })); + + while (stack.Count > 0) + { + var (currentPackage, currentPath) = stack.Pop(); + + if (currentPackage == destination) + { + dependencyPaths.Add(currentPath); + continue; + } + + var library = libraries.FirstOrDefault(lib => lib.Name == currentPackage); + if (library != null) + { + foreach (var dependency in library.Dependencies) + { + var dep = new Dependency + { + Name = dependency.Id, + Version = dependency.VersionRange.MinVersion.Version.ToString() + }; + + if (!visited.Contains(dep)) + { + visited.Add(dep); + var newPath = new List(currentPath) { dep }; + stack.Push((dependency.Id, newPath)); + } + } + } + } + + return dependencyPaths; + } + + private string GetPackageVersion(string packageName, IList libraries) + { + var library = libraries.FirstOrDefault(lib => lib.Name == packageName); + return library?.Version.ToString(); + } + } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs index b4e4625e256..657538d2243 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs @@ -48,7 +48,7 @@ public static void Register(CommandLineApplication app, Func getLogger, logger); var whyCommandRunner = getCommandRunner(); - whyCommandRunner.ExecuteCommandAsync(whyCommandArgs); + whyCommandRunner.ExecuteCommandAsync(whyCommandArgs); // exception when we try to step in here return 0; }); }); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj index 51082266367..ad4b8e0b41f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj @@ -22,9 +22,9 @@ - + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index 8adf56f2696..03146ce307e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -39,7 +39,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log) { #if DEBUG // Uncomment the following when debugging. Also uncomment the PackageReference for Microsoft.Build.Locator. - /*try + try { // .NET JIT compiles one method at a time. If this method calls `MSBuildLocator` directly, the // try block is never entered if Microsoft.Build.Locator.dll can't be found. So, run it in a @@ -50,7 +50,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log) { // MSBuildLocator is used only to enable Visual Studio debugging. // It's not needed when using a patched dotnet sdk, so it doesn't matter if it fails. - }*/ + } var debugNuGetXPlat = Environment.GetEnvironmentVariable("DEBUG_NUGET_XPLAT"); From 3d428bcc780a95df51a164d7a0f01918c538492c Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Tue, 2 Apr 2024 21:48:42 -0700 Subject: [PATCH 04/53] fixed duplicate paths bug --- .../PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index c48650a6678..72ae4d60e2c 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -145,8 +145,6 @@ private void FindDependencyGraphsForPackage(IEnumerable packa private List> GetDependencyGraphPerFramework(IEnumerable topLevelPackages, IList libraries, string destination) { var dependencyGraph = new List>(); - var listOfPaths = new List>(); - var visited = new HashSet(); foreach (var package in topLevelPackages) { @@ -159,7 +157,7 @@ private List> GetDependencyGraphPerFramework(IEnumerable> dependencyPathsInFramework = DfsTraversal(package.Name, libraries, visited, path, listOfPaths, destination); + List> dependencyPathsInFramework = DfsTraversal(package.Name, libraries, visited: new HashSet(), path, listOfPaths: new List>(), destination); dependencyGraph.AddRange(dependencyPathsInFramework); } return dependencyGraph; From db9a7ce608cb1664cf6df72595e9e9046c7ada3a Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 5 Apr 2024 04:42:06 -0700 Subject: [PATCH 05/53] trees are beautiful --- .../WhyCommand/WhyCommandRunner.cs | 362 +++++++++++++----- 1 file changed, 262 insertions(+), 100 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 72ae4d60e2c..8854436d669 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -6,10 +6,10 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.Build.Evaluation; using NuGet.ProjectModel; +using NuGet.Shared; namespace NuGet.CommandLine.XPlat { @@ -18,7 +18,13 @@ internal class WhyCommandRunner : IWhyCommandRunner private const string ProjectAssetsFile = "ProjectAssetsFile"; private const string ProjectName = "MSBuildProjectName"; - private const string LeafSymbol = "└──"; + private const string ChildNodeSymbol = "├─── "; + private const string LastChildNodeSymbol = "└─── "; + + private const string ChildPrefixSymbol = "│ "; + private const string LastChildPrefixSymbol = " "; + + private const string DuplicateTreeSymbol = "└─── (*)"; /// /// Use CLI arguments to execute why command. @@ -27,6 +33,8 @@ internal class WhyCommandRunner : IWhyCommandRunner /// public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) { + //TestMethod(); + // TODO: figure out how to use current directory if path is not passed in var projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") ? MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) @@ -89,7 +97,7 @@ public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) } else { - Console.Write($"Project '{projectName}' has the following dependency graph for '{package}'\n"); + Console.Write($"Project '{projectName}' has the following dependency graph(s) for '{package}'\n"); FindDependencyGraphsForPackage(packages, assetsFile.Targets, package); } } @@ -117,21 +125,26 @@ private void FindDependencyGraphsForPackage(IEnumerable packa { foreach (var frameworkPackages in packages) { - // Print framework name - string frameworkName = frameworkPackages.Framework; - Console.Write($"\n\t[{frameworkName}]:\n\n"); + LockFileTarget target = targetFrameworks.FirstOrDefault(i => i.TargetFramework.GetShortFolderName() == frameworkPackages.Framework); - // Get all the top level packages in the framework - IEnumerable frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; + if (target != null) + { + // print the framework name + Console.Write($"\n\t[{frameworkPackages.Framework}]:\n\n"); - // Get all the libraries in the framework - IList libraries = targetFrameworks - .FirstOrDefault(i => i.Name == frameworkPackages.Framework) - .Libraries; + // get all the top level packages for the framework + IEnumerable frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; - List> dependencyGraph = GetDependencyGraphPerFramework(frameworkTopLevelPackages, libraries, package); - //PrintDependencyGraphPerFramework(dependencyGraph); - AlternativePrintDependencyPaths(dependencyGraph); // TODO: this has duplicates, fix it + // get all package libraries for the framework + IList libraries = target.Libraries; + + List> dependencyGraph = OLDGetDependencyGraphPerFramework(frameworkTopLevelPackages, libraries, package); + //PrintDependencyGraphPerFramework(dependencyGraph); + AlternativePrintDependencyPaths(dependencyGraph); + + List dependencyGraph2 = GetDependencyGraphsPerFramework(frameworkTopLevelPackages, libraries, frameworkPackages, package); + PrintDependencyGraphsPerFramework(dependencyGraph2); + } } } @@ -142,27 +155,56 @@ private void FindDependencyGraphsForPackage(IEnumerable packa /// All libraries in a given project. /// The package name CLI argument. /// - private List> GetDependencyGraphPerFramework(IEnumerable topLevelPackages, IList libraries, string destination) + private List> OLDGetDependencyGraphPerFramework(IEnumerable topLevelPackages, IList libraries, string destination) { - var dependencyGraph = new List>(); + var dependencyGraph = new List>(); foreach (var package in topLevelPackages) { - List path = new List + List path = new List { - new Dependency() + new DependencyNode() { - Name = package.Name, + Id = package.Name, Version = package.OriginalRequestedVersion } }; - List> dependencyPathsInFramework = DfsTraversal(package.Name, libraries, visited: new HashSet(), path, listOfPaths: new List>(), destination); + List> dependencyPathsInFramework = OLDDfsTraversal(package.Name, libraries, visited: new HashSet(), path, listOfPaths: new List>(), destination); dependencyGraph.AddRange(dependencyPathsInFramework); } return dependencyGraph; } + /// + /// Returns a list of all top-level packages that have a transitive dependency on the given package + /// All top-level packages for a given framework. + /// All package libraries for a given framework. + /// The package we want the dependency paths for. + /// + private List GetDependencyGraphsPerFramework( + IEnumerable topLevelPackages, + IList packageLibraries, + FrameworkPackages frameworkPackages, + string targetPackage) + { + List dependencyGraph = null; + var visitedIdToVersion = new Dictionary(); + + foreach (var topLevelPackage in topLevelPackages) + { + DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visitedIdToVersion, targetPackage); + + if (dependencyNode != null) + { + dependencyGraph ??= new List(); + dependencyGraph.Add(dependencyNode); + } + } + + return dependencyGraph; + } + /// /// DFS from root node until destination is found (if destination ID exists) /// @@ -173,12 +215,12 @@ private List> GetDependencyGraphPerFramework(IEnumerableList of all dependency paths that lead to destination. /// CLI argument with the package that is passed in. /// - private List> DfsTraversal(string rootPackage, IList libraries, HashSet visited, List path, List> listOfPaths, string destination) + private List> OLDDfsTraversal(string rootPackage, IList libraries, HashSet visited, List path, List> listOfPaths, string destination) { if (rootPackage == destination) { // copy what is stored in list variable over to list that you allocate memory for - List pathToAdd = new List(); + List pathToAdd = new List(); foreach (var p in path) { pathToAdd.Add(p); @@ -188,23 +230,26 @@ private List> DfsTraversal(string rootPackage, IList i.Name == rootPackage); - var listDependencies = library.Dependencies; + LockFileTargetLibrary library = libraries?.FirstOrDefault(i => i.Name == rootPackage); + var listDependencies = library?.Dependencies; - if (listDependencies.Count != 0) + if (listDependencies?.Count != 0) { foreach (var dependency in listDependencies) { - Dependency dep; - dep.Name = dependency.Id; - dep.Version = dependency.VersionRange.MinVersion.Version.ToString(); + var dep = new DependencyNode() + { + Id = dependency.Id, + Version = dependency.VersionRange.MinVersion.Version.ToString() + }; + if (!visited.Contains(dep)) { visited.Add(dep); path.Add(dep); // recurse - DfsTraversal(dependency.Id, libraries, visited, path, listOfPaths, destination); + OLDDfsTraversal(dependency.Id, libraries, visited, path, listOfPaths, destination); // backtrack path.RemoveAt(path.Count - 1); @@ -217,35 +262,92 @@ private List> DfsTraversal(string rootPackage, IList - /// Print dependency graph with syntax/punctuation. + /// DFS from root node until destination is found (if destination ID exists) /// - /// List of all paths that lead to destination. - private void PrintDependencyGraphPerFramework(List> listOfPaths) + /// Top level package. + /// All libraries in the target framework. + /// A set to keep track of all nodes that have been visitedIdToVersion. + /// CLI argument with the package that is passed in. + /// + private DependencyNode FindDependencyPath( + string currentPackage, + IList libraries, + FrameworkPackages frameworkPackages, + Dictionary visitedIdToVersion, + string targetPackage) { - if (listOfPaths.Count == 0) + // If we find the target node, return the current node without any children + if (currentPackage == targetPackage) { - Console.Write("\t\tNo dependency paths found.\n"); + if (!visitedIdToVersion.ContainsKey(currentPackage)) + { + visitedIdToVersion.Add(currentPackage, GetResolvedVersion(currentPackage, frameworkPackages)); + } + + var currentNode = new DependencyNode + { + Id = currentPackage, + Version = visitedIdToVersion[currentPackage] + }; + + return currentNode; } - foreach (var path in listOfPaths) + // If we have already traversed this node's children and found paths, we don't want to traverse it again + if (visitedIdToVersion.ContainsKey(currentPackage)) { - Console.Write("\t\t"); - int iteration = 0; - foreach (var package in path) + var currentNode = new DependencyNode + { + Id = currentPackage, + Version = visitedIdToVersion[currentPackage], + IsDuplicate = true + }; + + return currentNode; + } + + // Find the library that matches the root package's ID, and get all its dependencies + var dependencies = libraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; + + if (dependencies?.Count != 0) + { + List paths = null; + + foreach (var dependency in dependencies) { - Console.Write($"{package.Name} ({package.Version})"); - // don't print arrows after the last package in the path - if (iteration < path.Count - 1) + var dependencyNode = FindDependencyPath(dependency.Id, libraries, frameworkPackages, visitedIdToVersion, targetPackage); + + // If the dependency has a path to the target, add it to the list of paths + if (dependencyNode != null) { - Console.Write(" -> "); + paths ??= new List(); + paths.Add(dependencyNode); } - iteration++; } - Console.Write("\n"); + + // If there are any paths leading to the target, return the current node with its children + if (paths?.Count > 0) + { + if (!visitedIdToVersion.ContainsKey(currentPackage)) + { + visitedIdToVersion.Add(currentPackage, GetResolvedVersion(currentPackage, frameworkPackages)); + } + + var currentNode = new DependencyNode + { + Id = currentPackage, + Version = visitedIdToVersion[currentPackage], + Children = paths + }; + return currentNode; + } } + + // If we found no paths leading to the target, return null + return null; } - private void AlternativePrintDependencyPaths(List> listOfPaths) + private void AlternativePrintDependencyPaths(List> listOfPaths) { if (listOfPaths.Count == 0) { @@ -257,26 +359,21 @@ private void AlternativePrintDependencyPaths(List> listOfPaths) foreach (var path in listOfPaths) { int iteration = 0; - var dependencyPathOutput = new StringBuilder(); foreach (var package in path) { if (iteration == 0) { - Console.Write($"{new string(' ', 12)}"); - - dependencyPathOutput.Append($"\t"); + Console.Write($"{new string(' ', 10)}{LastChildNodeSymbol} "); } else { - string indentation = new string(' ', iteration * 4); + string indentation = new string(' ', iteration * 5); - Console.Write($"\t{indentation}{LeafSymbol} "); - dependencyPathOutput.Append($"\t{indentation}{LeafSymbol} "); + Console.Write($"\t {indentation}{LastChildNodeSymbol} "); } - Console.Write($"{package.Name} ({package.Version})\n"); - dependencyPathOutput.Append($"{package.Name} ({package.Version})\n"); + Console.Write($"{package.Id} ({package.Version})\n"); iteration++; } @@ -285,66 +382,131 @@ private void AlternativePrintDependencyPaths(List> listOfPaths) } } - struct Dependency + private string GetResolvedVersion(string packageId, FrameworkPackages frameworkPackages) { - public string Name; - public string Version; + var packageReference = frameworkPackages.TopLevelPackages.FirstOrDefault(i => i.Name == packageId) + ?? frameworkPackages.TransitivePackages.FirstOrDefault(i => i.Name == packageId); + + return packageReference.ResolvedPackageMetadata.Identity.Version.ToNormalizedString(); } - private List> NonRecursiveDfsTraversal(string rootPackage, IList libraries, string destination) - { - var dependencyPaths = new List>(); - var stack = new Stack<(string package, List path)>(); - var visited = new HashSet(); - stack.Push((rootPackage, new List + class DependencyNode + { + public string Id { get; set; } + public string Version { get; set; } + // When a particular Node is a duplicate, we don't want to print its tree again, + // so we will print something else like a "(*)" instead + // See https://doc.rust-lang.org/cargo/commands/cargo-tree.html + // TODO: Are we doing this? + public bool IsDuplicate { get; set; } + public IList Children { get; set; } + + public override int GetHashCode() { - new Dependency - { - Name = rootPackage, - Version = GetPackageVersion(rootPackage, libraries) - } - })); + var hashCodeCombiner = new HashCodeCombiner(); + hashCodeCombiner.AddObject(Id); + hashCodeCombiner.AddSequence(Children); + return hashCodeCombiner.CombinedHash; + } + } - while (stack.Count > 0) - { - var (currentPackage, currentPath) = stack.Pop(); + private void TestMethod() + { + var A = new DependencyNode { Id = "A", Children = new List() }; + var B = new DependencyNode { Id = "B", Children = new List() }; + var C = new DependencyNode { Id = "C", Children = new List() }; + var D = new DependencyNode { Id = "D", Children = new List() }; + var E = new DependencyNode { Id = "E", Children = new List() }; + var F = new DependencyNode { Id = "F", Children = new List() }; + var G = new DependencyNode { Id = "G", Children = new List() }; + var H = new DependencyNode { Id = "H", Children = new List() }; + var I = new DependencyNode { Id = "I", Children = new List() }; + var J = new DependencyNode { Id = "J", Children = new List() }; + var K = new DependencyNode { Id = "L", Children = new List() }; + var L = new DependencyNode { Id = "L", Children = new List() }; + + /* + A.Children.Add(B); + B.Children.Add(C); + + A.Children.Add(D); + + E.Children.Add(F); + + var list = new List(); + list.Add(A); + list.Add(E); + */ + + A.Children.Add(B); + B.Children.Add(C); + B.Children.Add(G); + G.Children.Add(H); + + A.Children.Add(D); + + E.Children.Add(F); + + var list = new List(); + list.Add(A); + list.Add(E); + + + + Console.Write("\n\n"); + PrintDependencyGraphsPerFramework(list); + Console.Write("\n\n"); + } - if (currentPackage == destination) + private void PrintDependencyGraphsPerFramework(List nodes) + { + for (int i = 0; i < nodes.Count; i++) + { + var node = nodes[i]; + if (i == nodes.Count - 1) { - dependencyPaths.Add(currentPath); - continue; + PrintDependencyNode(node, "", true); } - - var library = libraries.FirstOrDefault(lib => lib.Name == currentPackage); - if (library != null) + else { - foreach (var dependency in library.Dependencies) - { - var dep = new Dependency - { - Name = dependency.Id, - Version = dependency.VersionRange.MinVersion.Version.ToString() - }; - - if (!visited.Contains(dep)) - { - visited.Add(dep); - var newPath = new List(currentPath) { dep }; - stack.Push((dependency.Id, newPath)); - } - } + PrintDependencyNode(node, "", false); } } - - return dependencyPaths; } - private string GetPackageVersion(string packageName, IList libraries) + private void PrintDependencyNode(DependencyNode node, string prefix, bool isLastChild) { - var library = libraries.FirstOrDefault(lib => lib.Name == packageName); - return library?.Version.ToString(); - } + string currentPrefix, nextPrefix; + if (isLastChild) + { + currentPrefix = prefix + LastChildNodeSymbol; + nextPrefix = prefix + LastChildPrefixSymbol; + } + else + { + currentPrefix = prefix + ChildNodeSymbol; + nextPrefix = prefix + ChildPrefixSymbol; + } + // print current node + Console.WriteLine($"{currentPrefix}{node.Id} v{node.Version}"); + + // if it is a duplicate, we do not print its tree again + if (node.IsDuplicate) + { + Console.WriteLine($"{nextPrefix}{DuplicateTreeSymbol}"); + return; + } + + if (node.Children?.Count > 0) + { + // recurse on the node's children + for (int i = 0; i < node.Children.Count; i++) + { + PrintDependencyNode(node.Children[i], nextPrefix, i == node.Children.Count - 1); + } + } + } } } From ef25ca2342c93c57a0c02a209a3b0e0130cc35a0 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 5 Apr 2024 06:00:22 -0700 Subject: [PATCH 06/53] rip my sleep schedule --- .../WhyCommand/WhyCommandRunner.cs | 290 ++++++------------ 1 file changed, 91 insertions(+), 199 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 8854436d669..f239b8192a2 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -97,8 +97,7 @@ public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) } else { - Console.Write($"Project '{projectName}' has the following dependency graph(s) for '{package}'\n"); - FindDependencyGraphsForPackage(packages, assetsFile.Targets, package); + FindAllDependencyGraphs(packages, assetsFile.Targets, package, projectName); } } } @@ -120,69 +119,54 @@ public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) /// /// All packages in the project. Split up by top level packages and transitive packages. /// All target frameworks in project and corresponding info about frameworks. - /// Package passed in as CLI argument. - private void FindDependencyGraphsForPackage(IEnumerable packages, IList targetFrameworks, string package) + /// The package we want the dependency paths for. + /// The name of the current project. + private void FindAllDependencyGraphs(IEnumerable packages, IList targetFrameworks, string package, string projectName) { + var dependencyGraphPerFramework = new Dictionary>(targetFrameworks.Count); + bool foundPathsToPackage = false; + foreach (var frameworkPackages in packages) { LockFileTarget target = targetFrameworks.FirstOrDefault(i => i.TargetFramework.GetShortFolderName() == frameworkPackages.Framework); if (target != null) { - // print the framework name - Console.Write($"\n\t[{frameworkPackages.Framework}]:\n\n"); - // get all the top level packages for the framework IEnumerable frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; // get all package libraries for the framework IList libraries = target.Libraries; - List> dependencyGraph = OLDGetDependencyGraphPerFramework(frameworkTopLevelPackages, libraries, package); - //PrintDependencyGraphPerFramework(dependencyGraph); - AlternativePrintDependencyPaths(dependencyGraph); + List dependencyGraph = GetDependencyGraphPerFramework(frameworkTopLevelPackages, libraries, frameworkPackages, package); + + if (dependencyGraph != null) + { + foundPathsToPackage = true; + } - List dependencyGraph2 = GetDependencyGraphsPerFramework(frameworkTopLevelPackages, libraries, frameworkPackages, package); - PrintDependencyGraphsPerFramework(dependencyGraph2); + dependencyGraphPerFramework.Add(frameworkPackages.Framework, dependencyGraph); } } - } - - /// - /// Find all dependency paths. - /// - /// "root nodes" of the graph. - /// All libraries in a given project. - /// The package name CLI argument. - /// - private List> OLDGetDependencyGraphPerFramework(IEnumerable topLevelPackages, IList libraries, string destination) - { - var dependencyGraph = new List>(); - foreach (var package in topLevelPackages) + if (!foundPathsToPackage) { - List path = new List - { - new DependencyNode() - { - Id = package.Name, - Version = package.OriginalRequestedVersion - } - }; - - List> dependencyPathsInFramework = OLDDfsTraversal(package.Name, libraries, visited: new HashSet(), path, listOfPaths: new List>(), destination); - dependencyGraph.AddRange(dependencyPathsInFramework); + Console.WriteLine($"Project '{projectName}' does not have any dependency graph(s) for '{package}'"); + } + else + { + Console.WriteLine($"Project '{projectName}' has the following dependency graph(s) for '{package}':"); + PrintAllDependencyGraphs(dependencyGraphPerFramework); } - return dependencyGraph; } /// - /// Returns a list of all top-level packages that have a transitive dependency on the given package + /// Returns a list of all top-level packages that have a dependency on the given package /// All top-level packages for a given framework. /// All package libraries for a given framework. /// The package we want the dependency paths for. /// - private List GetDependencyGraphsPerFramework( + private List GetDependencyGraphPerFramework( IEnumerable topLevelPackages, IList packageLibraries, FrameworkPackages frameworkPackages, @@ -206,72 +190,18 @@ private List GetDependencyGraphsPerFramework( } /// - /// DFS from root node until destination is found (if destination ID exists) - /// - /// Top level package. - /// All libraries in the target framework. - /// A set to keep track of all nodes that have been visited. - /// Keep track of path as DFS happens. - /// List of all dependency paths that lead to destination. - /// CLI argument with the package that is passed in. - /// - private List> OLDDfsTraversal(string rootPackage, IList libraries, HashSet visited, List path, List> listOfPaths, string destination) - { - if (rootPackage == destination) - { - // copy what is stored in list variable over to list that you allocate memory for - List pathToAdd = new List(); - foreach (var p in path) - { - pathToAdd.Add(p); - } - listOfPaths.Add(pathToAdd); - return listOfPaths; - } - - // Find the library that matches the root package's ID and get all its dependencies - LockFileTargetLibrary library = libraries?.FirstOrDefault(i => i.Name == rootPackage); - var listDependencies = library?.Dependencies; - - if (listDependencies?.Count != 0) - { - foreach (var dependency in listDependencies) - { - var dep = new DependencyNode() - { - Id = dependency.Id, - Version = dependency.VersionRange.MinVersion.Version.ToString() - }; - - if (!visited.Contains(dep)) - { - visited.Add(dep); - path.Add(dep); - - // recurse - OLDDfsTraversal(dependency.Id, libraries, visited, path, listOfPaths, destination); - - // backtrack - path.RemoveAt(path.Count - 1); - visited.Remove(dep); - } - } - } - - return listOfPaths; - } - - /// - /// DFS from root node until destination is found (if destination ID exists) + /// Recursive method that traverses the current node looking for a path to the target node. + /// Returns null if no path was found. /// - /// Top level package. - /// All libraries in the target framework. - /// A set to keep track of all nodes that have been visitedIdToVersion. - /// CLI argument with the package that is passed in. + /// Current 'root' package. + /// All libraries in the target framework. + /// All resolved package references, used to get packages' resolved versions. + /// A dictionary mapping all visited packageIds to their resolved versions. + /// The package we want the dependency paths for. /// private DependencyNode FindDependencyPath( string currentPackage, - IList libraries, + IList packageLibraries, FrameworkPackages frameworkPackages, Dictionary visitedIdToVersion, string targetPackage) @@ -307,7 +237,7 @@ private DependencyNode FindDependencyPath( } // Find the library that matches the root package's ID, and get all its dependencies - var dependencies = libraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; + var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; if (dependencies?.Count != 0) { @@ -315,7 +245,7 @@ private DependencyNode FindDependencyPath( foreach (var dependency in dependencies) { - var dependencyNode = FindDependencyPath(dependency.Id, libraries, frameworkPackages, visitedIdToVersion, targetPackage); + var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visitedIdToVersion, targetPackage); // If the dependency has a path to the target, add it to the list of paths if (dependencyNode != null) @@ -347,130 +277,73 @@ private DependencyNode FindDependencyPath( return null; } - private void AlternativePrintDependencyPaths(List> listOfPaths) + private string GetResolvedVersion(string packageId, FrameworkPackages frameworkPackages) { - if (listOfPaths.Count == 0) - { - Console.Write("\t\tNo dependency paths found.\n"); - } + var packageReference = frameworkPackages.TopLevelPackages.FirstOrDefault(i => i.Name == packageId) + ?? frameworkPackages.TransitivePackages.FirstOrDefault(i => i.Name == packageId); - Console.OutputEncoding = System.Text.Encoding.UTF8; // Set UTF-8 encoding + return packageReference.ResolvedPackageMetadata.Identity.Version.ToNormalizedString(); + } + + private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) + { + // We do not want to print separate trees for differen frameworks if they're exactly the same, so we will deduplicate them + var printed = new HashSet(dependencyGraphPerFramework.Count); - foreach (var path in listOfPaths) + foreach (var framework in dependencyGraphPerFramework.Keys) { - int iteration = 0; + if (printed.Contains(framework)) + { + continue; + } + + var frameworks = new List { framework }; + printed.Add(framework); - foreach (var package in path) + foreach (var potentialDuplicate in dependencyGraphPerFramework.Keys) { - if (iteration == 0) + if (potentialDuplicate == framework) { - Console.Write($"{new string(' ', 10)}{LastChildNodeSymbol} "); + continue; } - else - { - string indentation = new string(' ', iteration * 5); - Console.Write($"\t {indentation}{LastChildNodeSymbol} "); + // TODO deduplication isn't working + if (dependencyGraphPerFramework[framework].GetHashCode() == dependencyGraphPerFramework[potentialDuplicate].GetHashCode()) + { + frameworks.Add(potentialDuplicate); + printed.Add(potentialDuplicate); } - - Console.Write($"{package.Id} ({package.Version})\n"); - - iteration++; } - Console.Write("\n"); + PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[framework]); } } - private string GetResolvedVersion(string packageId, FrameworkPackages frameworkPackages) - { - var packageReference = frameworkPackages.TopLevelPackages.FirstOrDefault(i => i.Name == packageId) - ?? frameworkPackages.TransitivePackages.FirstOrDefault(i => i.Name == packageId); - - return packageReference.ResolvedPackageMetadata.Identity.Version.ToNormalizedString(); - } - - - class DependencyNode + private void PrintDependencyGraphPerFramework(List frameworks, List nodes) { - public string Id { get; set; } - public string Version { get; set; } - // When a particular Node is a duplicate, we don't want to print its tree again, - // so we will print something else like a "(*)" instead - // See https://doc.rust-lang.org/cargo/commands/cargo-tree.html - // TODO: Are we doing this? - public bool IsDuplicate { get; set; } - public IList Children { get; set; } - - public override int GetHashCode() + foreach (var framework in frameworks) { - var hashCodeCombiner = new HashCodeCombiner(); - hashCodeCombiner.AddObject(Id); - hashCodeCombiner.AddSequence(Children); - return hashCodeCombiner.CombinedHash; + Console.WriteLine($"\n\t[{framework}]"); } - } - - private void TestMethod() - { - var A = new DependencyNode { Id = "A", Children = new List() }; - var B = new DependencyNode { Id = "B", Children = new List() }; - var C = new DependencyNode { Id = "C", Children = new List() }; - var D = new DependencyNode { Id = "D", Children = new List() }; - var E = new DependencyNode { Id = "E", Children = new List() }; - var F = new DependencyNode { Id = "F", Children = new List() }; - var G = new DependencyNode { Id = "G", Children = new List() }; - var H = new DependencyNode { Id = "H", Children = new List() }; - var I = new DependencyNode { Id = "I", Children = new List() }; - var J = new DependencyNode { Id = "J", Children = new List() }; - var K = new DependencyNode { Id = "L", Children = new List() }; - var L = new DependencyNode { Id = "L", Children = new List() }; - - /* - A.Children.Add(B); - B.Children.Add(C); - - A.Children.Add(D); - - E.Children.Add(F); - - var list = new List(); - list.Add(A); - list.Add(E); - */ - - A.Children.Add(B); - B.Children.Add(C); - B.Children.Add(G); - G.Children.Add(H); - - A.Children.Add(D); - - E.Children.Add(F); - - var list = new List(); - list.Add(A); - list.Add(E); - + if (nodes == null || nodes.Count == 0) + { + Console.WriteLine("\n\t No dependency graphs found."); + return; + } - Console.Write("\n\n"); - PrintDependencyGraphsPerFramework(list); - Console.Write("\n\n"); - } + Console.WriteLine($"\t {ChildPrefixSymbol}"); - private void PrintDependencyGraphsPerFramework(List nodes) - { for (int i = 0; i < nodes.Count; i++) { var node = nodes[i]; if (i == nodes.Count - 1) { - PrintDependencyNode(node, "", true); + PrintDependencyNode(node, "\t ", true); } else { - PrintDependencyNode(node, "", false); + PrintDependencyNode(node, "\t ", false); } } } @@ -490,7 +363,7 @@ private void PrintDependencyNode(DependencyNode node, string prefix, bool isLast } // print current node - Console.WriteLine($"{currentPrefix}{node.Id} v{node.Version}"); + Console.WriteLine($"{currentPrefix}{node.Id} (v{node.Version})"); // if it is a duplicate, we do not print its tree again if (node.IsDuplicate) @@ -508,5 +381,24 @@ private void PrintDependencyNode(DependencyNode node, string prefix, bool isLast } } } + + class DependencyNode + { + public string Id { get; set; } + public string Version { get; set; } + // When a particular Node is a duplicate, we don't want to print its tree again, + // so we will print something else like a "(*)" instead + // See https://doc.rust-lang.org/cargo/commands/cargo-tree.html + public bool IsDuplicate { get; set; } + public IList Children { get; set; } + + public override int GetHashCode() + { + var hashCodeCombiner = new HashCodeCombiner(); + hashCodeCombiner.AddObject(Id); + hashCodeCombiner.AddSequence(Children); + return hashCodeCombiner.CombinedHash; + } + } } } From 9170c265d3e9ae1ca8bbd1eade4b59e219be83ed Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 5 Apr 2024 16:42:14 -0700 Subject: [PATCH 07/53] everything works now --- .../WhyCommand/WhyCommandRunner.cs | 110 ++++++++++++++---- 1 file changed, 88 insertions(+), 22 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index f239b8192a2..3700d711ef4 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -6,6 +6,8 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; +using System.Security.Policy; using System.Threading.Tasks; using Microsoft.Build.Evaluation; using NuGet.ProjectModel; @@ -155,7 +157,7 @@ private void FindAllDependencyGraphs(IEnumerable packages, IL } else { - Console.WriteLine($"Project '{projectName}' has the following dependency graph(s) for '{package}':"); + Console.WriteLine($"Project '{projectName}' has the following dependency graph(s) for '{package}':\n"); PrintAllDependencyGraphs(dependencyGraphPerFramework); } } @@ -287,53 +289,79 @@ private string GetResolvedVersion(string packageId, FrameworkPackages frameworkP private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) { - // We do not want to print separate trees for differen frameworks if they're exactly the same, so we will deduplicate them + // If different frameworks have the same dependency graphs, we want to deduplicate them var printed = new HashSet(dependencyGraphPerFramework.Count); + var deduplicatedFrameworks = GetDeduplicatedFrameworks(dependencyGraphPerFramework); + + foreach (var frameworks in deduplicatedFrameworks) + { + PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[frameworks.FirstOrDefault()]); + } + } + + private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) + { + List frameworksWithoutGraphs = null; + var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); + + var dependencyGraphsToFrameworks = new Dictionary, List>(dependencyGraphPerFramework.Count); + foreach (var framework in dependencyGraphPerFramework.Keys) { - if (printed.Contains(framework)) + if (dependencyGraphPerFramework[framework] == null) { + frameworksWithoutGraphs ??= new List(); + frameworksWithoutGraphs.Add(framework); continue; } - var frameworks = new List { framework }; - printed.Add(framework); - - foreach (var potentialDuplicate in dependencyGraphPerFramework.Keys) + int hash = GetDependencyGraphHashCode(dependencyGraphPerFramework[framework]); + if (dependencyGraphHashes.ContainsKey(hash)) { - if (potentialDuplicate == framework) - { - continue; - } + dependencyGraphHashes[hash].Add(framework); + } + else + { + dependencyGraphHashes.Add(hash, new List { framework }); + } - // TODO deduplication isn't working - if (dependencyGraphPerFramework[framework].GetHashCode() == dependencyGraphPerFramework[potentialDuplicate].GetHashCode()) - { - frameworks.Add(potentialDuplicate); - printed.Add(potentialDuplicate); - } + List currentGraph = dependencyGraphPerFramework[framework]; + if (dependencyGraphsToFrameworks.TryGetValue(currentGraph, out var frameworkList)) + { + frameworkList.Add(framework); } + else + { + dependencyGraphsToFrameworks.Add(currentGraph, new List { framework }); + } + } + + var deduplicatedFrameworks = dependencyGraphHashes.Values.ToList(); - PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[framework]); + if (frameworksWithoutGraphs != null) + { + deduplicatedFrameworks.Add(frameworksWithoutGraphs); } + + return deduplicatedFrameworks; } private void PrintDependencyGraphPerFramework(List frameworks, List nodes) { foreach (var framework in frameworks) { - Console.WriteLine($"\n\t[{framework}]"); + Console.WriteLine($"\t[{framework}]"); } + Console.WriteLine($"\t {ChildPrefixSymbol}"); + if (nodes == null || nodes.Count == 0) { - Console.WriteLine("\n\t No dependency graphs found."); + Console.WriteLine($"\t {LastChildNodeSymbol}No dependency graphs found\n"); return; } - Console.WriteLine($"\t {ChildPrefixSymbol}"); - for (int i = 0; i < nodes.Count; i++) { var node = nodes[i]; @@ -346,6 +374,7 @@ private void PrintDependencyGraphPerFramework(List frameworks, List graph) + { + var hashCodeCombiner = new HashCodeCombiner(); + hashCodeCombiner.AddSequence(graph); + return hashCodeCombiner.CombinedHash; + } + class DependencyNode { public string Id { get; set; } @@ -396,9 +432,39 @@ public override int GetHashCode() { var hashCodeCombiner = new HashCodeCombiner(); hashCodeCombiner.AddObject(Id); + hashCodeCombiner.AddObject(Version); + hashCodeCombiner.AddObject(IsDuplicate); hashCodeCombiner.AddSequence(Children); return hashCodeCombiner.CombinedHash; } + + public bool Equals(DependencyNode other) + { + if (other is null) return false; + + return Id == other.Id && + Version == other.Version && + IsDuplicate == other.IsDuplicate && + Children.SequenceEqualWithNullCheck(other.Children); + } + + public override bool Equals(object obj) + { + return Equals(obj as DependencyNode); + } + + public static bool operator ==(DependencyNode x, DependencyNode y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + + return x.Equals(y); + } + + public static bool operator !=(DependencyNode x, DependencyNode y) + { + return !(x == y); + } } } } From b1b74f5033e78139df1b2e3bff3c1ded52cc11df Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 5 Apr 2024 20:02:15 -0700 Subject: [PATCH 08/53] refactoring --- .../WhyCommand/IWhyCommandRunner.cs | 2 +- .../WhyCommand/WhyCommandArgs.cs | 10 +- .../WhyCommand/WhyCommandRunner.cs | 329 +++++++++--------- .../Commands/WhyCommand.cs | 2 +- 4 files changed, 169 insertions(+), 174 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs index 73ee83b4084..8815b00631f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs @@ -7,6 +7,6 @@ namespace NuGet.CommandLine.XPlat { internal interface IWhyCommandRunner { - Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs); + Task ExecuteCommand(WhyCommandArgs whyCommandArgs); } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs index 8a4e71968d8..59cfada9850 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs @@ -15,13 +15,11 @@ internal class WhyCommandArgs public List Frameworks { get; } /// - /// A constructor for the arguments of the 'why' - /// command. This is used to execute the runner's - /// method + /// A constructor for the arguments of the 'why' command. /// - /// The path to the solution or project file - /// The package for which we show the dependency graph - /// The target frameworks for which we show the dependency graph + /// The path to the solution or project file. + /// The package for which we show the dependency graphs. + /// The target frameworks for which we show the dependency graphs. /// public WhyCommandArgs( string path, diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 3700d711ef4..56f3c253994 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -6,8 +6,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; -using System.Security.Policy; using System.Threading.Tasks; using Microsoft.Build.Evaluation; using NuGet.ProjectModel; @@ -29,104 +27,106 @@ internal class WhyCommandRunner : IWhyCommandRunner private const string DuplicateTreeSymbol = "└─── (*)"; /// - /// Use CLI arguments to execute why command. + /// Execute 'why' command. /// - /// CLI arguments. + /// CLI arguments for the 'why' command. /// - public Task ExecuteCommandAsync(WhyCommandArgs whyCommandArgs) + public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { - //TestMethod(); - // TODO: figure out how to use current directory if path is not passed in var projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") ? MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) : new List() { whyCommandArgs.Path }; - // the package you want to print the dependency paths for - string package = whyCommandArgs.Package; + string targetPackage = whyCommandArgs.Package; var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); foreach (var projectPath in projectPaths) { - // Open project to evaluate properties for the assets - // file and the name of the project Project project = MSBuildAPIUtility.GetProject(projectPath); + // if the current project is not a PackageReference project, print an error message and continue to the next project if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) { - Console.Error.WriteLine(string.Format(CultureInfo.CurrentCulture, - Strings.Error_NotPRProject, - projectPath)); - Console.WriteLine(); + Console.Error.WriteLine( + string.Format( + CultureInfo.CurrentCulture, + Strings.Error_NotPRProject, + projectPath)); + + ProjectCollection.GlobalProjectCollection.UnloadProject(project); continue; } string projectName = project.GetPropertyValue(ProjectName); string assetsPath = project.GetPropertyValue(ProjectAssetsFile); - // If the file was not found, print an error message and continue to next project + // if the assets file was not found, print an error message and continue to the next project if (!File.Exists(assetsPath)) { - Console.Error.WriteLine(string.Format(CultureInfo.CurrentCulture, - Strings.Error_AssetsFileNotFound, - projectPath)); - Console.WriteLine(); + Console.Error.WriteLine( + string.Format( + CultureInfo.CurrentCulture, + Strings.Error_AssetsFileNotFound, + projectPath)); + + ProjectCollection.GlobalProjectCollection.UnloadProject(project); + continue; } - 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(); + LockFile assetsFile = lockFileFormat.Read(assetsPath); - // Get all the packages that are referenced in a project - List packages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true); - - // 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) - { - // No packages means that no package references at all were found in the current framework - if (!packages.Any()) - { - Console.WriteLine(string.Format(Strings.WhyCommand_Error_NoPackagesFoundForFrameworks, projectName)); - } - else - { - FindAllDependencyGraphs(packages, assetsFile.Targets, package, projectName); - } - } - } - else - { - Console.WriteLine(string.Format(Strings.ListPkg_ErrorReadingAssetsFile, assetsPath)); - } + // assets file validation + if (assetsFile.PackageSpec == null + || assetsFile.Targets == null + || assetsFile.Targets.Count == 0) + { + Console.Error.WriteLine( + string.Format( + CultureInfo.CurrentCulture, + Strings.ListPkg_ErrorReadingAssetsFile, + assetsPath)); - // Unload project ProjectCollection.GlobalProjectCollection.UnloadProject(project); + continue; } + + // get all resolved package references for a project + List packages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true); + + if (packages?.Count > 0) + { + FindAllDependencyGraphs(packages, assetsFile.Targets, targetPackage, projectName); + } + else + { + Console.WriteLine(string.Format(Strings.WhyCommand_Error_NoPackagesFoundForFrameworks, projectName)); + } + + // unload project + ProjectCollection.GlobalProjectCollection.UnloadProject(project); } return Task.CompletedTask; } /// - /// Run the why command, print out output to console. + /// Run the 'why' command, and print out output to console. /// - /// All packages in the project. Split up by top level packages and transitive packages. - /// All target frameworks in project and corresponding info about frameworks. - /// The package we want the dependency paths for. + /// All packages in the project, split up by top-level packages and transitive packages. + /// All target frameworks in the project. + /// The package we want the dependency paths for. /// The name of the current project. - private void FindAllDependencyGraphs(IEnumerable packages, IList targetFrameworks, string package, string projectName) + private void FindAllDependencyGraphs( + IEnumerable packages, + IList targetFrameworks, + string targetPackage, + string projectName) { var dependencyGraphPerFramework = new Dictionary>(targetFrameworks.Count); - bool foundPathsToPackage = false; + bool doesProjectHaveDependencyOnPackage = false; foreach (var frameworkPackages in packages) { @@ -134,38 +134,40 @@ private void FindAllDependencyGraphs(IEnumerable packages, IL if (target != null) { - // get all the top level packages for the framework + // get all the top-level packages for the framework IEnumerable frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; // get all package libraries for the framework IList libraries = target.Libraries; - List dependencyGraph = GetDependencyGraphPerFramework(frameworkTopLevelPackages, libraries, frameworkPackages, package); + List dependencyGraph = GetDependencyGraphPerFramework(frameworkTopLevelPackages, libraries, frameworkPackages, targetPackage); if (dependencyGraph != null) { - foundPathsToPackage = true; + doesProjectHaveDependencyOnPackage = true; } dependencyGraphPerFramework.Add(frameworkPackages.Framework, dependencyGraph); } } - if (!foundPathsToPackage) + if (!doesProjectHaveDependencyOnPackage) { - Console.WriteLine($"Project '{projectName}' does not have any dependency graph(s) for '{package}'"); + Console.WriteLine($"Project '{projectName}' does not have any dependency graph(s) for '{targetPackage}'"); } else { - Console.WriteLine($"Project '{projectName}' has the following dependency graph(s) for '{package}':\n"); + Console.WriteLine($"Project '{projectName}' has the following dependency graph(s) for '{targetPackage}':\n"); PrintAllDependencyGraphs(dependencyGraphPerFramework); } } + /// + /// Returns a list of all top-level packages that have a dependency on the target package /// - /// Returns a list of all top-level packages that have a dependency on the given package /// All top-level packages for a given framework. - /// All package libraries for a given framework. + /// All package libraries for a given framework. + /// All resolved package references for a given framework. Used to get a dependency's resolved version for the graph. /// The package we want the dependency paths for. /// private List GetDependencyGraphPerFramework( @@ -175,15 +177,18 @@ private List GetDependencyGraphPerFramework( string targetPackage) { List dependencyGraph = null; + + // a dictionary mapping all 'visited' packageIds to their resolved versions var visitedIdToVersion = new Dictionary(); foreach (var topLevelPackage in topLevelPackages) { + // use depth-first search to find dependency paths to the target package DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visitedIdToVersion, targetPackage); if (dependencyNode != null) { - dependencyGraph ??= new List(); + dependencyGraph ??= []; dependencyGraph.Add(dependencyNode); } } @@ -197,7 +202,7 @@ private List GetDependencyGraphPerFramework( /// /// Current 'root' package. /// All libraries in the target framework. - /// All resolved package references, used to get packages' resolved versions. + /// All resolved package references for a given framework, used to get packages' resolved versions. /// A dictionary mapping all visited packageIds to their resolved versions. /// The package we want the dependency paths for. /// @@ -208,7 +213,7 @@ private DependencyNode FindDependencyPath( Dictionary visitedIdToVersion, string targetPackage) { - // If we find the target node, return the current node without any children + // if we reach the target node, return the current node without any children if (currentPackage == targetPackage) { if (!visitedIdToVersion.ContainsKey(currentPackage)) @@ -225,7 +230,10 @@ private DependencyNode FindDependencyPath( return currentNode; } - // If we have already traversed this node's children and found paths, we don't want to traverse it again + // if we have already traversed this node's children and found dependency paths, we don't want to traverse it again + // TODO: Are we traversing paths multiple times for the same package if we don't find any dependency paths? + // We can have 2 separate sets: one that just tracks all visited nodes in a HashSet, and another that tracks all nodes + // that have been added to the dependency graph (similar to the current visitedIdToVersion dictionary) if (visitedIdToVersion.ContainsKey(currentPackage)) { var currentNode = new DependencyNode @@ -238,7 +246,7 @@ private DependencyNode FindDependencyPath( return currentNode; } - // Find the library that matches the root package's ID, and get all its dependencies + // find the library that matches the root package's ID, and get all its dependencies var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; if (dependencies?.Count != 0) @@ -249,15 +257,15 @@ private DependencyNode FindDependencyPath( { var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visitedIdToVersion, targetPackage); - // If the dependency has a path to the target, add it to the list of paths + // if the dependency has a path to the target, add it to the list of paths if (dependencyNode != null) { - paths ??= new List(); + paths ??= []; paths.Add(dependencyNode); } } - // If there are any paths leading to the target, return the current node with its children + // if there are any paths leading to the target, return the current node with its children if (paths?.Count > 0) { if (!visitedIdToVersion.ContainsKey(currentPackage)) @@ -271,28 +279,39 @@ private DependencyNode FindDependencyPath( Version = visitedIdToVersion[currentPackage], Children = paths }; + return currentNode; } } - // If we found no paths leading to the target, return null + // if we found no paths leading to the target, return null return null; } + /// + /// Gets the resolved version of a given packageId in the current target framework's graph. + /// + /// The package we want the version for. + /// All resolved package references for a given framework. + /// private string GetResolvedVersion(string packageId, FrameworkPackages frameworkPackages) { var packageReference = frameworkPackages.TopLevelPackages.FirstOrDefault(i => i.Name == packageId) ?? frameworkPackages.TransitivePackages.FirstOrDefault(i => i.Name == packageId); + // TODO: Validation sanity check here? + return packageReference.ResolvedPackageMetadata.Identity.Version.ToNormalizedString(); } + /// + /// Prints the dependency graphs for all target frameworks. + /// + /// A dictionary mapping target frameworks to their dependency graphs. private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) { - // If different frameworks have the same dependency graphs, we want to deduplicate them - var printed = new HashSet(dependencyGraphPerFramework.Count); - - var deduplicatedFrameworks = GetDeduplicatedFrameworks(dependencyGraphPerFramework); + // deduplicate the dependency graphs + List> deduplicatedFrameworks = GetDeduplicatedFrameworks(dependencyGraphPerFramework); foreach (var frameworks in deduplicatedFrameworks) { @@ -300,54 +319,12 @@ private void PrintAllDependencyGraphs(Dictionary> d } } - private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) - { - List frameworksWithoutGraphs = null; - var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); - - var dependencyGraphsToFrameworks = new Dictionary, List>(dependencyGraphPerFramework.Count); - - foreach (var framework in dependencyGraphPerFramework.Keys) - { - if (dependencyGraphPerFramework[framework] == null) - { - frameworksWithoutGraphs ??= new List(); - frameworksWithoutGraphs.Add(framework); - continue; - } - - int hash = GetDependencyGraphHashCode(dependencyGraphPerFramework[framework]); - if (dependencyGraphHashes.ContainsKey(hash)) - { - dependencyGraphHashes[hash].Add(framework); - } - else - { - dependencyGraphHashes.Add(hash, new List { framework }); - } - - List currentGraph = dependencyGraphPerFramework[framework]; - if (dependencyGraphsToFrameworks.TryGetValue(currentGraph, out var frameworkList)) - { - frameworkList.Add(framework); - } - else - { - dependencyGraphsToFrameworks.Add(currentGraph, new List { framework }); - } - } - - var deduplicatedFrameworks = dependencyGraphHashes.Values.ToList(); - - if (frameworksWithoutGraphs != null) - { - deduplicatedFrameworks.Add(frameworksWithoutGraphs); - } - - return deduplicatedFrameworks; - } - - private void PrintDependencyGraphPerFramework(List frameworks, List nodes) + /// + /// Prints the dependency graph for a given framework/list of frameworks. + /// + /// The list of frameworks that share this dependency graph. + /// The top-level package nodes of the dependency graph. + private void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes) { foreach (var framework in frameworks) { @@ -356,27 +333,33 @@ private void PrintDependencyGraphPerFramework(List frameworks, List + /// Prints a single dependency node on a line. + /// + /// The current package node. + /// The prefix we need to print before the current node. + /// Specifies whether the current node is the last child of its parent. private void PrintDependencyNode(DependencyNode node, string prefix, bool isLastChild) { string currentPrefix, nextPrefix; @@ -411,6 +394,51 @@ private void PrintDependencyNode(DependencyNode node, string prefix, bool isLast } } + /// + /// Deduplicates dependency graphs, and returns groups of frameworks that share the same graph. + /// + /// A dictionary mapping target frameworks to their dependency graphs. + /// + private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) + { + List frameworksWithoutGraphs = null; + var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); + + foreach (var framework in dependencyGraphPerFramework.Keys) + { + if (dependencyGraphPerFramework[framework] == null) + { + frameworksWithoutGraphs ??= []; + frameworksWithoutGraphs.Add(framework); + continue; + } + + int hash = GetDependencyGraphHashCode(dependencyGraphPerFramework[framework]); + if (dependencyGraphHashes.ContainsKey(hash)) + { + dependencyGraphHashes[hash].Add(framework); + } + else + { + dependencyGraphHashes.Add(hash, [framework]); + } + } + + var deduplicatedFrameworks = dependencyGraphHashes.Values.ToList(); + + if (frameworksWithoutGraphs != null) + { + deduplicatedFrameworks.Add(frameworksWithoutGraphs); + } + + return deduplicatedFrameworks; + } + + /// + /// Returns a hash for a given dependency graph. Used to deduplicate dependency graphs for different frameworks. + /// + /// The dependency graph for a given framework. + /// private int GetDependencyGraphHashCode(IList graph) { var hashCodeCombiner = new HashCodeCombiner(); @@ -422,10 +450,7 @@ class DependencyNode { public string Id { get; set; } public string Version { get; set; } - // When a particular Node is a duplicate, we don't want to print its tree again, - // so we will print something else like a "(*)" instead - // See https://doc.rust-lang.org/cargo/commands/cargo-tree.html - public bool IsDuplicate { get; set; } + public bool IsDuplicate { get; set; } // When a particular Node is a duplicate, we don't want to print its tree again public IList Children { get; set; } public override int GetHashCode() @@ -437,34 +462,6 @@ public override int GetHashCode() hashCodeCombiner.AddSequence(Children); return hashCodeCombiner.CombinedHash; } - - public bool Equals(DependencyNode other) - { - if (other is null) return false; - - return Id == other.Id && - Version == other.Version && - IsDuplicate == other.IsDuplicate && - Children.SequenceEqualWithNullCheck(other.Children); - } - - public override bool Equals(object obj) - { - return Equals(obj as DependencyNode); - } - - public static bool operator ==(DependencyNode x, DependencyNode y) - { - if (ReferenceEquals(x, y)) return true; - if (x is null || y is null) return false; - - return x.Equals(y); - } - - public static bool operator !=(DependencyNode x, DependencyNode y) - { - return !(x == y); - } } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs index 657538d2243..f808faad582 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs @@ -48,7 +48,7 @@ public static void Register(CommandLineApplication app, Func getLogger, logger); var whyCommandRunner = getCommandRunner(); - whyCommandRunner.ExecuteCommandAsync(whyCommandArgs); // exception when we try to step in here + whyCommandRunner.ExecuteCommand(whyCommandArgs); return 0; }); }); From 067430a8dcaa5349b901fd77b7ca688c961b45d2 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Sat, 6 Apr 2024 00:40:28 -0700 Subject: [PATCH 09/53] cleaning up strings --- .../WhyCommand/WhyCommandArgs.cs | 2 +- .../WhyCommand/WhyCommandRunner.cs | 31 +++++++--- .../Commands/WhyCommand.cs | 41 ++++++++++--- .../Strings.Designer.cs | 59 ++++++++++++++++--- .../NuGet.CommandLine.XPlat/Strings.resx | 39 ++++++++---- 5 files changed, 140 insertions(+), 32 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs index 59cfada9850..d3028b03acc 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs @@ -19,7 +19,7 @@ internal class WhyCommandArgs /// /// The path to the solution or project file. /// The package for which we show the dependency graphs. - /// The target frameworks for which we show the dependency graphs. + /// The target framework(s) for which we show the dependency graphs. /// public WhyCommandArgs( string path, diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 56f3c253994..0edb7fed3d6 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -18,6 +18,7 @@ internal class WhyCommandRunner : IWhyCommandRunner private const string ProjectAssetsFile = "ProjectAssetsFile"; private const string ProjectName = "MSBuildProjectName"; + // Dependency graph console output symbols private const string ChildNodeSymbol = "├─── "; private const string LastChildNodeSymbol = "└─── "; @@ -33,7 +34,6 @@ internal class WhyCommandRunner : IWhyCommandRunner /// public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { - // TODO: figure out how to use current directory if path is not passed in var projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") ? MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) : new List() { whyCommandArgs.Path }; @@ -86,7 +86,7 @@ public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) Console.Error.WriteLine( string.Format( CultureInfo.CurrentCulture, - Strings.ListPkg_ErrorReadingAssetsFile, + Strings.WhyCommand_Error_CannotReadAssetsFile, assetsPath)); ProjectCollection.GlobalProjectCollection.UnloadProject(project); @@ -102,7 +102,10 @@ public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) } else { - Console.WriteLine(string.Format(Strings.WhyCommand_Error_NoPackagesFoundForFrameworks, projectName)); + Console.WriteLine( + string.Format( + Strings.WhyCommand_Message_NoPackagesFoundForFramework, + projectName)); } // unload project @@ -153,11 +156,20 @@ private void FindAllDependencyGraphs( if (!doesProjectHaveDependencyOnPackage) { - Console.WriteLine($"Project '{projectName}' does not have any dependency graph(s) for '{targetPackage}'"); + Console.WriteLine( + string.Format( + Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, + projectName, + targetPackage)); } else { - Console.WriteLine($"Project '{projectName}' has the following dependency graph(s) for '{targetPackage}':\n"); + Console.WriteLine( + string.Format( + Strings.WhyCommand_Message_DependencyGraphsFoundInProject, + projectName, + targetPackage)); + PrintAllDependencyGraphs(dependencyGraphPerFramework); } } @@ -310,6 +322,8 @@ private string GetResolvedVersion(string packageId, FrameworkPackages frameworkP /// A dictionary mapping target frameworks to their dependency graphs. private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) { + Console.WriteLine(); + // deduplicate the dependency graphs List> deduplicatedFrameworks = GetDeduplicatedFrameworks(dependencyGraphPerFramework); @@ -335,7 +349,7 @@ private void PrintDependencyGraphPerFramework(List frameworks, List frameworks, List /// A dictionary mapping target frameworks to their dependency graphs. - /// + /// + /// eg. { { "net6.0", "netcoreapp3.1" }, { "net472" } } + /// private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) { List frameworksWithoutGraphs = null; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs index f808faad582..ec8bf99e229 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.IO; using System.Linq; using Microsoft.Extensions.CommandLineUtils; using NuGet.Common; @@ -10,7 +11,7 @@ namespace NuGet.CommandLine.XPlat { - internal class WhyCommand + internal static class WhyCommand { public static void Register(CommandLineApplication app, Func getLogger, Func getCommandRunner) { @@ -20,7 +21,7 @@ public static void Register(CommandLineApplication app, Func getLogger, why.HelpOption(XPlatUtility.HelpOption); CommandArgument path = why.Argument( - "", + " | ", Strings.WhyCommand_PathArgument_Description, multipleValues: false); @@ -30,13 +31,13 @@ public static void Register(CommandLineApplication app, Func getLogger, multipleValues: false); CommandOption frameworks = why.Option( - "--framework", - Strings.WhyCommand_FrameworkArgument_Description, + "-f|--framework", + Strings.WhyCommand_FrameworkOption_Description, CommandOptionType.MultipleValue); why.OnExecute(() => { - // TODO: Can path be empty? + ValidatePathArgument(path); ValidatePackageArgument(package); ValidateFrameworksOption(frameworks); @@ -54,11 +55,34 @@ public static void Register(CommandLineApplication app, Func getLogger, }); } + private static void ValidatePathArgument(CommandArgument path) + { + if (string.IsNullOrEmpty(path.Value)) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + path.Name)); + } + + if (!File.Exists(path.Value) + || (!path.Value.EndsWith("proj", StringComparison.OrdinalIgnoreCase) && !path.Value.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_PathIsMissingOrInvalid, + path.Value)); + } + } + private static void ValidatePackageArgument(CommandArgument package) { if (string.IsNullOrEmpty(package.Value)) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.WhyCommand_Error_ArgumentCannotBeEmpty, package.Name)); + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + package.Name)); } } @@ -72,7 +96,10 @@ private static void ValidateFrameworksOption(CommandOption framework) if (frameworks.Any(f => f.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase))) { - throw new ArgumentException(Strings.ListPkg_InvalidFramework, nameof(framework)); + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_InvalidFramework, + framework.Template)); } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 6cac3553d60..211b9cb2305 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -2224,25 +2224,70 @@ internal static string WhyCommand_Error_ArgumentCannotBeEmpty { } /// - /// Looks up a localized string similar to No packages were found for this framework.. + /// Looks up a localized string similar to Unable to read the assets file `{0}`.. /// - internal static string WhyCommand_Error_NoPackagesFoundForFrameworks { + internal static string WhyCommand_Error_CannotReadAssetsFile { get { - return ResourceManager.GetString("WhyCommand_Error_NoPackagesFoundForFrameworks", resourceCulture); + return ResourceManager.GetString("WhyCommand_Error_CannotReadAssetsFile", resourceCulture); } } /// - /// Looks up a localized string similar to Specifies the target framework to look-up in the dependency graph while searching for a package.. + /// Looks up a localized string similar to Failed to parse one of the given frameworks. Please make sure the given frameworks are valid.. + /// + internal static string WhyCommand_Error_InvalidFramework { + get { + return ResourceManager.GetString("WhyCommand_Error_InvalidFramework", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to run 'dotnet nuget why'. Missing or invalid project/solution file '{0}'.. + /// + internal static string WhyCommand_Error_PathIsMissingOrInvalid { + get { + return ResourceManager.GetString("WhyCommand_Error_PathIsMissingOrInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The target framework(s) for which dependency graphs are shown.. + /// + internal static string WhyCommand_FrameworkOption_Description { + get { + return ResourceManager.GetString("WhyCommand_FrameworkOption_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project '{0}' has the following dependency graph(s) for '{1}':. + /// + internal static string WhyCommand_Message_DependencyGraphsFoundInProject { + get { + return ResourceManager.GetString("WhyCommand_Message_DependencyGraphsFoundInProject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project '{0}' does not have any dependency graph(s) for '{1}'. + /// + internal static string WhyCommand_Message_NoDependencyGraphsFoundInProject { + get { + return ResourceManager.GetString("WhyCommand_Message_NoDependencyGraphsFoundInProject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No packages were found for this framework.. /// - internal static string WhyCommand_FrameworkArgument_Description { + internal static string WhyCommand_Message_NoPackagesFoundForFramework { get { - return ResourceManager.GetString("WhyCommand_FrameworkArgument_Description", resourceCulture); + return ResourceManager.GetString("WhyCommand_Message_NoPackagesFoundForFramework", resourceCulture); } } /// - /// Looks up a localized string similar to A package name to look-up in the dependency graph.. + /// Looks up a localized string similar to The package name to look-up in the dependency graph.. /// internal static string WhyCommand_PackageArgument_Description { get { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 7fe15306186..4ad145403a1 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -912,23 +912,42 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Gets the NuGet configuration settings that will be applied. - - A package name to look-up in the dependency graph. - - - Specifies the target framework to look-up in the dependency graph while searching for a package. - Lists the dependency graph for a particular package for a given project or solution. A path to a project, solution file or directory. - - No packages were found for this framework. + + The package name to look-up in the dependency graph. + + + The target framework(s) for which dependency graphs are shown. Unable to run 'dotnet nuget why'. The '{0}' argument cannot be empty. - {0} - Argument which was not provided + {0} - Argument that was not provided + + + Unable to run 'dotnet nuget why'. Missing or invalid project/solution file '{0}'. + {0} - Project/solution file path + + + Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. + + + Unable to read the assets file `{0}`. + {0} - Assets file path + + + No packages were found for this framework. + + + Project '{0}' does not have any dependency graph(s) for '{1}' + {0} - Project name, {1} - Target package + + + Project '{0}' has the following dependency graph(s) for '{1}': + {0} - Project name, {1} - Target package - \ No newline at end of file + From 08e95fc5ca3ec0facd42cb6b109954d6d03a48e1 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Sat, 6 Apr 2024 18:38:01 -0700 Subject: [PATCH 10/53] fixed redundant traversal bug --- .../WhyCommand/WhyCommandRunner.cs | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 0edb7fed3d6..e5681a9622a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -190,13 +190,15 @@ private List GetDependencyGraphPerFramework( { List dependencyGraph = null; - // a dictionary mapping all 'visited' packageIds to their resolved versions - var visitedIdToVersion = new Dictionary(); + // hashset tracking every package node that we've traversed + var visited = new HashSet(); + // dictionary tracking all package nodes that have been added to the graph, mapped to their resolved versions + var addedToGraph = new Dictionary(); foreach (var topLevelPackage in topLevelPackages) { // use depth-first search to find dependency paths to the target package - DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visitedIdToVersion, targetPackage); + DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visited, addedToGraph, targetPackage); if (dependencyNode != null) { @@ -215,49 +217,58 @@ private List GetDependencyGraphPerFramework( /// Current 'root' package. /// All libraries in the target framework. /// All resolved package references for a given framework, used to get packages' resolved versions. - /// A dictionary mapping all visited packageIds to their resolved versions. + /// HashSet tracking every package node that we've traversed. + /// Dictionary tracking all packageIds that were added to the graph, mapped to their resolved versions. /// The package we want the dependency paths for. /// private DependencyNode FindDependencyPath( string currentPackage, IList packageLibraries, FrameworkPackages frameworkPackages, - Dictionary visitedIdToVersion, + HashSet visited, + Dictionary addedToGraph, string targetPackage) { // if we reach the target node, return the current node without any children if (currentPackage == targetPackage) { - if (!visitedIdToVersion.ContainsKey(currentPackage)) + if (!addedToGraph.ContainsKey(currentPackage)) { - visitedIdToVersion.Add(currentPackage, GetResolvedVersion(currentPackage, frameworkPackages)); + addedToGraph.Add(currentPackage, GetResolvedVersion(currentPackage, frameworkPackages)); } var currentNode = new DependencyNode { Id = currentPackage, - Version = visitedIdToVersion[currentPackage] + Version = addedToGraph[currentPackage] }; return currentNode; } - // if we have already traversed this node's children and found dependency paths, we don't want to traverse it again - // TODO: Are we traversing paths multiple times for the same package if we don't find any dependency paths? - // We can have 2 separate sets: one that just tracks all visited nodes in a HashSet, and another that tracks all nodes - // that have been added to the dependency graph (similar to the current visitedIdToVersion dictionary) - if (visitedIdToVersion.ContainsKey(currentPackage)) + // if we have already traversed this node's children and found dependency paths, mark it as a duplicate node and return + if (addedToGraph.ContainsKey(currentPackage)) { var currentNode = new DependencyNode { Id = currentPackage, - Version = visitedIdToVersion[currentPackage], + Version = addedToGraph[currentPackage], IsDuplicate = true }; return currentNode; } + // if we have already traversed this node's children and found no dependency paths, return null + if (visited.Contains(currentPackage)) + { + return null; + } + else + { + visited.Add(currentPackage); + } + // find the library that matches the root package's ID, and get all its dependencies var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; @@ -265,9 +276,10 @@ private DependencyNode FindDependencyPath( { List paths = null; + // recurse on the package's dependencies foreach (var dependency in dependencies) { - var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visitedIdToVersion, targetPackage); + var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visited, addedToGraph, targetPackage); // if the dependency has a path to the target, add it to the list of paths if (dependencyNode != null) @@ -280,15 +292,15 @@ private DependencyNode FindDependencyPath( // if there are any paths leading to the target, return the current node with its children if (paths?.Count > 0) { - if (!visitedIdToVersion.ContainsKey(currentPackage)) + if (!addedToGraph.ContainsKey(currentPackage)) { - visitedIdToVersion.Add(currentPackage, GetResolvedVersion(currentPackage, frameworkPackages)); + addedToGraph.Add(currentPackage, GetResolvedVersion(currentPackage, frameworkPackages)); } var currentNode = new DependencyNode { Id = currentPackage, - Version = visitedIdToVersion[currentPackage], + Version = addedToGraph[currentPackage], Children = paths }; @@ -311,8 +323,6 @@ private string GetResolvedVersion(string packageId, FrameworkPackages frameworkP var packageReference = frameworkPackages.TopLevelPackages.FirstOrDefault(i => i.Name == packageId) ?? frameworkPackages.TransitivePackages.FirstOrDefault(i => i.Name == packageId); - // TODO: Validation sanity check here? - return packageReference.ResolvedPackageMetadata.Identity.Version.ToNormalizedString(); } From ae50998f354b154d2d326b644d0f2fcb38380abd Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Mon, 8 Apr 2024 01:10:21 -0700 Subject: [PATCH 11/53] minor string stuff --- .../WhyCommand/WhyCommandRunner.cs | 8 ++++---- .../NuGet.CommandLine.XPlat/Strings.Designer.cs | 9 +++++++++ src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx | 3 +++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index e5681a9622a..e3c4bd0bc35 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -25,7 +25,7 @@ internal class WhyCommandRunner : IWhyCommandRunner private const string ChildPrefixSymbol = "│ "; private const string LastChildPrefixSymbol = " "; - private const string DuplicateTreeSymbol = "└─── (*)"; + private const string DuplicateTreeSymbol = $"{LastChildNodeSymbol}(*)"; /// /// Execute 'why' command. @@ -197,7 +197,7 @@ private List GetDependencyGraphPerFramework( foreach (var topLevelPackage in topLevelPackages) { - // use depth-first search to find dependency paths to the target package + // use depth-first search to find dependency paths from the top-level package to the target package DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visited, addedToGraph, targetPackage); if (dependencyNode != null) @@ -229,7 +229,7 @@ private DependencyNode FindDependencyPath( Dictionary addedToGraph, string targetPackage) { - // if we reach the target node, return the current node without any children + // if we reach the target node, return if (currentPackage == targetPackage) { if (!addedToGraph.ContainsKey(currentPackage)) @@ -359,7 +359,7 @@ private void PrintDependencyGraphPerFramework(List frameworks, List + /// Looks up a localized string similar to No dependency graph(s) found. + /// + internal static string WhyCommand_Message_NoDependencyGraphsFoundForFramework { + get { + return ResourceManager.GetString("WhyCommand_Message_NoDependencyGraphsFoundForFramework", resourceCulture); + } + } + /// /// Looks up a localized string similar to Project '{0}' does not have any dependency graph(s) for '{1}'. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 4ad145403a1..4c34feccd2a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -950,4 +950,7 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Project '{0}' has the following dependency graph(s) for '{1}': {0} - Project name, {1} - Target package + + No dependency graph(s) found + From 3f153f956f593e0f41143a0cd1036270dccaf24e Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 10 Apr 2024 02:16:49 -0700 Subject: [PATCH 12/53] remove (*) deduplication, fix project reference bug --- .../WhyCommand/WhyCommandRunner.cs | 144 +++++++++--------- 1 file changed, 70 insertions(+), 74 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index e3c4bd0bc35..31357daff18 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -25,10 +25,8 @@ internal class WhyCommandRunner : IWhyCommandRunner private const string ChildPrefixSymbol = "│ "; private const string LastChildPrefixSymbol = " "; - private const string DuplicateTreeSymbol = $"{LastChildNodeSymbol}(*)"; - /// - /// Execute 'why' command. + /// Executes the 'why' command. /// /// CLI arguments for the 'why' command. /// @@ -116,41 +114,46 @@ public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) } /// - /// Run the 'why' command, and print out output to console. + /// Runs the 'why' command, and prints out output to console. /// - /// All packages in the project, split up by top-level packages and transitive packages. - /// All target frameworks in the project. + /// All frameworks in the project, with their top-level packages and transitive packages. + /// All lock file targets in the project. /// The package we want the dependency paths for. /// The name of the current project. private void FindAllDependencyGraphs( - IEnumerable packages, - IList targetFrameworks, + IEnumerable frameworkPackages, + IList targets, string targetPackage, string projectName) { - var dependencyGraphPerFramework = new Dictionary>(targetFrameworks.Count); + var dependencyGraphPerFramework = new Dictionary>(targets.Count); bool doesProjectHaveDependencyOnPackage = false; - foreach (var frameworkPackages in packages) + foreach (var frameworkPackage in frameworkPackages) { - LockFileTarget target = targetFrameworks.FirstOrDefault(i => i.TargetFramework.GetShortFolderName() == frameworkPackages.Framework); + LockFileTarget target = targets.FirstOrDefault(f => f.TargetFramework.GetShortFolderName() == frameworkPackage.Framework); if (target != null) { // get all the top-level packages for the framework - IEnumerable frameworkTopLevelPackages = frameworkPackages.TopLevelPackages; + IEnumerable frameworkTopLevelPackages = frameworkPackage.TopLevelPackages; // get all package libraries for the framework - IList libraries = target.Libraries; + IList packageLibraries = target.Libraries; - List dependencyGraph = GetDependencyGraphPerFramework(frameworkTopLevelPackages, libraries, frameworkPackages, targetPackage); + List dependencyGraph = null; - if (dependencyGraph != null) + if (packageLibraries.Any(l => l.Name == targetPackage)) { - doesProjectHaveDependencyOnPackage = true; + dependencyGraph = GetDependencyGraphPerFramework(packageLibraries, frameworkPackage, targetPackage); + + if (dependencyGraph != null) + { + doesProjectHaveDependencyOnPackage = true; + } } - dependencyGraphPerFramework.Add(frameworkPackages.Framework, dependencyGraph); + dependencyGraphPerFramework.Add(frameworkPackage.Framework, dependencyGraph); } } @@ -177,13 +180,11 @@ private void FindAllDependencyGraphs( /// /// Returns a list of all top-level packages that have a dependency on the target package /// - /// All top-level packages for a given framework. /// All package libraries for a given framework. - /// All resolved package references for a given framework. Used to get a dependency's resolved version for the graph. + /// All frameworks in the project, with their top-level packages and transitive packages. /// The package we want the dependency paths for. /// private List GetDependencyGraphPerFramework( - IEnumerable topLevelPackages, IList packageLibraries, FrameworkPackages frameworkPackages, string targetPackage) @@ -192,13 +193,18 @@ private List GetDependencyGraphPerFramework( // hashset tracking every package node that we've traversed var visited = new HashSet(); - // dictionary tracking all package nodes that have been added to the graph, mapped to their resolved versions - var addedToGraph = new Dictionary(); + // dictionary tracking all package nodes that have been added to the graph, mapped to their DependencyNode objects + var addedToGraph = new Dictionary(); + // dictionary mapping packageIds to their resolved versions + Dictionary versions = GetAllResolvedVersions(packageLibraries); + + // get all the top-level packages for the framework + IEnumerable topLevelPackages = frameworkPackages.TopLevelPackages; foreach (var topLevelPackage in topLevelPackages) { // use depth-first search to find dependency paths from the top-level package to the target package - DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visited, addedToGraph, targetPackage); + DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visited, addedToGraph, versions, targetPackage); if (dependencyNode != null) { @@ -215,10 +221,11 @@ private List GetDependencyGraphPerFramework( /// Returns null if no path was found. /// /// Current 'root' package. - /// All libraries in the target framework. - /// All resolved package references for a given framework, used to get packages' resolved versions. + /// All package libraries for a given framework. + /// All frameworks in the project, with their top-level packages and transitive packages. /// HashSet tracking every package node that we've traversed. - /// Dictionary tracking all packageIds that were added to the graph, mapped to their resolved versions. + /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. + /// Dictionary mapping packageIds to their resolved versions. /// The package we want the dependency paths for. /// private DependencyNode FindDependencyPath( @@ -226,7 +233,8 @@ private DependencyNode FindDependencyPath( IList packageLibraries, FrameworkPackages frameworkPackages, HashSet visited, - Dictionary addedToGraph, + Dictionary addedToGraph, + Dictionary versions, string targetPackage) { // if we reach the target node, return @@ -234,29 +242,21 @@ private DependencyNode FindDependencyPath( { if (!addedToGraph.ContainsKey(currentPackage)) { - addedToGraph.Add(currentPackage, GetResolvedVersion(currentPackage, frameworkPackages)); + addedToGraph.Add(currentPackage, + new DependencyNode + { + Id = currentPackage, + Version = versions[currentPackage] + }); } - var currentNode = new DependencyNode - { - Id = currentPackage, - Version = addedToGraph[currentPackage] - }; - - return currentNode; + return addedToGraph[currentPackage]; } // if we have already traversed this node's children and found dependency paths, mark it as a duplicate node and return if (addedToGraph.ContainsKey(currentPackage)) { - var currentNode = new DependencyNode - { - Id = currentPackage, - Version = addedToGraph[currentPackage], - IsDuplicate = true - }; - - return currentNode; + return addedToGraph[currentPackage]; } // if we have already traversed this node's children and found no dependency paths, return null @@ -264,10 +264,8 @@ private DependencyNode FindDependencyPath( { return null; } - else - { - visited.Add(currentPackage); - } + + visited.Add(currentPackage); // find the library that matches the root package's ID, and get all its dependencies var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; @@ -279,7 +277,7 @@ private DependencyNode FindDependencyPath( // recurse on the package's dependencies foreach (var dependency in dependencies) { - var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visited, addedToGraph, targetPackage); + var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visited, addedToGraph, versions, targetPackage); // if the dependency has a path to the target, add it to the list of paths if (dependencyNode != null) @@ -294,17 +292,16 @@ private DependencyNode FindDependencyPath( { if (!addedToGraph.ContainsKey(currentPackage)) { - addedToGraph.Add(currentPackage, GetResolvedVersion(currentPackage, frameworkPackages)); + addedToGraph.Add(currentPackage, + new DependencyNode + { + Id = currentPackage, + Version = versions[currentPackage], + Children = paths + }); } - var currentNode = new DependencyNode - { - Id = currentPackage, - Version = addedToGraph[currentPackage], - Children = paths - }; - - return currentNode; + return addedToGraph[currentPackage]; } } @@ -313,17 +310,21 @@ private DependencyNode FindDependencyPath( } /// - /// Gets the resolved version of a given packageId in the current target framework's graph. + /// Adds all resolved versions of packages to a dictionary. /// - /// The package we want the version for. - /// All resolved package references for a given framework. + /// All package libraries for a given framework. /// - private string GetResolvedVersion(string packageId, FrameworkPackages frameworkPackages) + private Dictionary GetAllResolvedVersions(IList packageLibraries) { - var packageReference = frameworkPackages.TopLevelPackages.FirstOrDefault(i => i.Name == packageId) - ?? frameworkPackages.TransitivePackages.FirstOrDefault(i => i.Name == packageId); + var versions = new Dictionary(); + + foreach (var package in packageLibraries) + { + versions.Add(package.Name, package.Version.ToNormalizedString()); + } - return packageReference.ResolvedPackageMetadata.Identity.Version.ToNormalizedString(); + // Should we distinguish between package references and project references? + return versions; } /// @@ -402,13 +403,6 @@ private void PrintDependencyNode(DependencyNode node, string prefix, bool isLast // print current node Console.WriteLine($"{currentPrefix}{node.Id} (v{node.Version})"); - // if it is a duplicate, we do not print its tree again - if (node.IsDuplicate) - { - Console.WriteLine($"{nextPrefix}{DuplicateTreeSymbol}"); - return; - } - if (node.Children?.Count > 0) { // recurse on the node's children @@ -473,19 +467,21 @@ private int GetDependencyGraphHashCode(IList graph) return hashCodeCombiner.CombinedHash; } - class DependencyNode + private class DependencyNode { public string Id { get; set; } public string Version { get; set; } - public bool IsDuplicate { get; set; } // When a particular Node is a duplicate, we don't want to print its tree again public IList Children { get; set; } + // Should we distinguish between package references and project references in the output? + + // DependencyNode Parent; for stack + public override int GetHashCode() { var hashCodeCombiner = new HashCodeCombiner(); hashCodeCombiner.AddObject(Id); hashCodeCombiner.AddObject(Version); - hashCodeCombiner.AddObject(IsDuplicate); hashCodeCombiner.AddSequence(Children); return hashCodeCombiner.CombinedHash; } From 8a750f19884b3997702fb990f67a656e67621ef3 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Tue, 16 Apr 2024 13:41:41 -0700 Subject: [PATCH 13/53] wip: recursion -> stack --- .../WhyCommand/WhyCommandRunner.cs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 31357daff18..36969b080e0 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -486,5 +486,165 @@ public override int GetHashCode() return hashCodeCombiner.CombinedHash; } } + + private List ALT_STACK_GetDependencyGraphPerFramework( + IList packageLibraries, + FrameworkPackages frameworkPackages, + string targetPackage) + { + List dependencyGraph = null; + + // hashset tracking every package node that we've traversed + var visited = new HashSet(); + // dictionary tracking all package nodes that have been added to the graph, mapped to their DependencyNode objects + var addedToGraph = new Dictionary(); + // dictionary mapping all packageIds to their resolved version + Dictionary versions = GetAllResolvedVersions(packageLibraries); + + // get all the top-level packages for the framework + IEnumerable topLevelPackages = frameworkPackages.TopLevelPackages; + + foreach (var topLevelPackage in topLevelPackages) + { + // use depth-first search to find dependency paths from the top-level package to the target package + DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visited, addedToGraph, versions, targetPackage); + + if (dependencyNode != null) + { + dependencyGraph ??= []; + dependencyGraph.Add(dependencyNode); + } + } + + return dependencyGraph; + } + + private DependencyNode ALT_STACK_FindDependencyPath( + string topLevelPackage, + IList packageLibraries, + FrameworkPackages frameworkPackages, + HashSet visited, + Dictionary addedToGraph, + Dictionary versions, + string targetPackage) + { + DependencyNode topLevelNode = null; + var stack = new Stack(); + stack.Push(new StackData(topLevelPackage, null)); + + while (stack.Count > 0) + { + var currentPackage = stack.Pop(); + + // if we reach the target node, return + if (currentPackage.Id == targetPackage) + { + if (!addedToGraph.ContainsKey(currentPackage.Id)) + { + addedToGraph.Add(currentPackage, + new DependencyNode + { + Id = currentPackage.Id, + Version = versions[currentPackage] + }); + } + + AddToGraph(topLevelNode, currentPackage); + return addedToGraph[currentPackage]; + } + + // if we have already traversed this node's children and found dependency paths, mark it as a duplicate node and return + if (addedToGraph.ContainsKey(currentPackage)) + { + return addedToGraph[currentPackage]; + } + + // if we have already traversed this node's children and found no dependency paths, return null + if (visited.Contains(currentPackage)) + { + return null; + } + + visited.Add(currentPackage); + + // find the library that matches the root package's ID, and get all its dependencies + var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; + + if (dependencies?.Count != 0) + { + List paths = null; + + // recurse on the package's dependencies + foreach (var dependency in dependencies) + { + var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visited, addedToGraph, versions, targetPackage); + + // if the dependency has a path to the target, add it to the list of paths + if (dependencyNode != null) + { + paths ??= []; + paths.Add(dependencyNode); + } + } + + // if there are any paths leading to the target, return the current node with its children + if (paths?.Count > 0) + { + if (!addedToGraph.ContainsKey(currentPackage)) + { + addedToGraph.Add(currentPackage, + new DependencyNode + { + Id = currentPackage, + Version = versions[currentPackage], + Children = paths + }); + } + + return addedToGraph[currentPackage]; + } + } + + // if we found no paths leading to the target, return null + return null; + } + + return topLevelNode; + } + + private void AddToGraph(DependencyNode topLevelNode, StackData targetPackageInfo) + { + topLevelNode ??= new DependencyNode(); + + var dependencyPath = new List(); + + dependencyPath.Add(targetPackageInfo.Id); + StackData current = targetPackageInfo.Parent; + + while (current != null) + { + dependencyPath.Add(current.Id); + current = current.Parent; + } + + DependencyNode currentNode = topLevelNode; + + for (int i = dependencyPath.Count - 1; i >= 0; i--) + { + currentNode + } + } + + private class StackData + { + public string Id { get; set; } + public StackData Parent { get; set; } + + public StackData(string currentId, StackData parent) + { + Id = currentId; + Parent = parent; + } + } } } From 25804a8f4fbfa3b53402bd494fdd07e9a91e3a86 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 17 Apr 2024 01:45:19 -0700 Subject: [PATCH 14/53] recursion -> stack is working now --- .../WhyCommand/WhyCommandRunner.cs | 517 ++++++++---------- 1 file changed, 220 insertions(+), 297 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 36969b080e0..d1a707f2d93 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -126,8 +126,8 @@ private void FindAllDependencyGraphs( string targetPackage, string projectName) { - var dependencyGraphPerFramework = new Dictionary>(targets.Count); bool doesProjectHaveDependencyOnPackage = false; + var dependencyGraphPerFramework = new Dictionary>(targets.Count); foreach (var frameworkPackage in frameworkPackages) { @@ -141,19 +141,17 @@ private void FindAllDependencyGraphs( // get all package libraries for the framework IList packageLibraries = target.Libraries; - List dependencyGraph = null; - if (packageLibraries.Any(l => l.Name == targetPackage)) { - dependencyGraph = GetDependencyGraphPerFramework(packageLibraries, frameworkPackage, targetPackage); + doesProjectHaveDependencyOnPackage = true; - if (dependencyGraph != null) - { - doesProjectHaveDependencyOnPackage = true; - } + dependencyGraphPerFramework.Add(frameworkPackage.Framework, + GetDependencyGraphPerFramework(frameworkPackage.TopLevelPackages, packageLibraries, targetPackage)); + } + else + { + dependencyGraphPerFramework.Add(frameworkPackage.Framework, null); } - - dependencyGraphPerFramework.Add(frameworkPackage.Framework, dependencyGraph); } } @@ -178,15 +176,15 @@ private void FindAllDependencyGraphs( } /// - /// Returns a list of all top-level packages that have a dependency on the target package + /// Returns all dependency paths from the top-level packages to the target package for a given framework. /// + /// All top-level packages for the framework. /// All package libraries for a given framework. - /// All frameworks in the project, with their top-level packages and transitive packages. /// The package we want the dependency paths for. /// private List GetDependencyGraphPerFramework( + IEnumerable topLevelPackages, IList packageLibraries, - FrameworkPackages frameworkPackages, string targetPackage) { List dependencyGraph = null; @@ -194,17 +192,14 @@ private List GetDependencyGraphPerFramework( // hashset tracking every package node that we've traversed var visited = new HashSet(); // dictionary tracking all package nodes that have been added to the graph, mapped to their DependencyNode objects - var addedToGraph = new Dictionary(); - // dictionary mapping packageIds to their resolved versions + var dependencyNodes = new Dictionary(); + // dictionary mapping all packageIds to their resolved version Dictionary versions = GetAllResolvedVersions(packageLibraries); - // get all the top-level packages for the framework - IEnumerable topLevelPackages = frameworkPackages.TopLevelPackages; - foreach (var topLevelPackage in topLevelPackages) { // use depth-first search to find dependency paths from the top-level package to the target package - DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visited, addedToGraph, versions, targetPackage); + DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, visited, dependencyNodes, versions, targetPackage); if (dependencyNode != null) { @@ -216,115 +211,150 @@ private List GetDependencyGraphPerFramework( return dependencyGraph; } + /// - /// Recursive method that traverses the current node looking for a path to the target node. - /// Returns null if no path was found. + /// Adds all resolved versions of packages to a dictionary. /// - /// Current 'root' package. /// All package libraries for a given framework. - /// All frameworks in the project, with their top-level packages and transitive packages. + /// + private Dictionary GetAllResolvedVersions(IList packageLibraries) + { + var versions = new Dictionary(); + + foreach (var package in packageLibraries) + { + versions.Add(package.Name, package.Version.ToNormalizedString()); + } + + return versions; + } + + /// + /// Traverses the dependency graph for a given top-level package, looking for a path to the target package. + /// Returns the top-level package node if a path was found, and null if no path was found. + /// + /// Top-level package. + /// All package libraries for a given framework. /// HashSet tracking every package node that we've traversed. - /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. + /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. /// Dictionary mapping packageIds to their resolved versions. /// The package we want the dependency paths for. /// private DependencyNode FindDependencyPath( - string currentPackage, + string topLevelPackage, IList packageLibraries, - FrameworkPackages frameworkPackages, HashSet visited, - Dictionary addedToGraph, + Dictionary dependencyNodes, Dictionary versions, string targetPackage) { - // if we reach the target node, return - if (currentPackage == targetPackage) + var stack = new Stack(); + stack.Push(new StackDependencyData(topLevelPackage, null)); + + while (stack.Count > 0) { - if (!addedToGraph.ContainsKey(currentPackage)) + var currentPackageData = stack.Pop(); + var currentPackageId = currentPackageData.Id; + + // if we reach the target node, or if we've already found dependency paths from the current package, add it to the graph + if (currentPackageId == targetPackage + || dependencyNodes.ContainsKey(currentPackageId)) { - addedToGraph.Add(currentPackage, - new DependencyNode - { - Id = currentPackage, - Version = versions[currentPackage] - }); + AddToGraph(currentPackageData, dependencyNodes, versions); + continue; + } + + // if we have already traversed this node's children, continue + if (visited.Contains(currentPackageId)) + { + continue; } - return addedToGraph[currentPackage]; + visited.Add(currentPackageId); + + // get all dependencies for the current package + var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackageId)?.Dependencies; + + if (dependencies?.Count != 0) + { + // push all the dependencies onto the stack + foreach (var dependency in dependencies) + { + stack.Push(new StackDependencyData(dependency.Id, currentPackageData)); + } + } } - // if we have already traversed this node's children and found dependency paths, mark it as a duplicate node and return - if (addedToGraph.ContainsKey(currentPackage)) + if (dependencyNodes.ContainsKey(topLevelPackage)) { - return addedToGraph[currentPackage]; + return dependencyNodes[topLevelPackage]; } - - // if we have already traversed this node's children and found no dependency paths, return null - if (visited.Contains(currentPackage)) + else { return null; } + } - visited.Add(currentPackage); + /// + /// Adds a dependency path to the graph, starting from the target package and traversing up to the top-level package. + /// + /// Target node data. This stores parent references, so it can be used to construct the dependency graph + /// up to the top-level package. + /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. + /// Dictionary mapping packageIds to their resolved versions. + private void AddToGraph( + StackDependencyData targetPackageData, + Dictionary dependencyNodes, + Dictionary versions) + { + // first, we traverse the target's parents, listing the packages in the path from the target to the top-level package + var dependencyPath = new List { targetPackageData.Id }; + StackDependencyData current = targetPackageData.Parent; - // find the library that matches the root package's ID, and get all its dependencies - var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; + while (current != null) + { + dependencyPath.Add(current.Id); + current = current.Parent; + } - if (dependencies?.Count != 0) + // then, we traverse this list from the target package to the top-level package, adding/updating their dependency nodes in the graph + for (int i = 0; i < dependencyPath.Count; i++) { - List paths = null; + string currentPackageId = dependencyPath[i]; - // recurse on the package's dependencies - foreach (var dependency in dependencies) + if (!dependencyNodes.ContainsKey(currentPackageId)) { - var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visited, addedToGraph, versions, targetPackage); + dependencyNodes.Add(currentPackageId, + new DependencyNode + { + Id = currentPackageId, + Version = versions[currentPackageId], + Children = new HashSet() + }); + } - // if the dependency has a path to the target, add it to the list of paths - if (dependencyNode != null) + if (i > 0) + { + var childNode = dependencyNodes[dependencyPath[i - 1]]; + + /* + * Why does this not work, even though hashCodes are the same? + * How can I fix the Equals override method to make this work? + if (dependencyNodes[currentPackageId].Children.Contains(childNode)) { - paths ??= []; - paths.Add(dependencyNode); + continue; } - } + */ - // if there are any paths leading to the target, return the current node with its children - if (paths?.Count > 0) - { - if (!addedToGraph.ContainsKey(currentPackage)) + // TODO: Replace with .Contains() once Equals override method is fixed + if (dependencyNodes[currentPackageId].Children.Any(p => p.Id == childNode.Id)) { - addedToGraph.Add(currentPackage, - new DependencyNode - { - Id = currentPackage, - Version = versions[currentPackage], - Children = paths - }); + continue; } - return addedToGraph[currentPackage]; + dependencyNodes[currentPackageId].Children.Add(childNode); } } - - // if we found no paths leading to the target, return null - return null; - } - - /// - /// Adds all resolved versions of packages to a dictionary. - /// - /// All package libraries for a given framework. - /// - private Dictionary GetAllResolvedVersions(IList packageLibraries) - { - var versions = new Dictionary(); - - foreach (var package in packageLibraries) - { - versions.Add(package.Name, package.Version.ToNormalizedString()); - } - - // Should we distinguish between package references and project references? - return versions; } /// @@ -344,75 +374,6 @@ private void PrintAllDependencyGraphs(Dictionary> d } } - /// - /// Prints the dependency graph for a given framework/list of frameworks. - /// - /// The list of frameworks that share this dependency graph. - /// The top-level package nodes of the dependency graph. - private void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes) - { - foreach (var framework in frameworks) - { - Console.WriteLine($"\t[{framework}]"); - } - - Console.WriteLine($"\t {ChildPrefixSymbol}"); - - if (topLevelNodes == null || topLevelNodes.Count == 0) - { - Console.WriteLine($"\t {LastChildNodeSymbol}{Strings.WhyCommand_Message_NoDependencyGraphsFoundForFramework}\n"); - return; - } - - for (int i = 0; i < topLevelNodes.Count; i++) - { - var node = topLevelNodes[i]; - if (i == topLevelNodes.Count - 1) - { - PrintDependencyNode(node, prefix: "\t ", isLastChild: true); - } - else - { - PrintDependencyNode(node, prefix: "\t ", isLastChild: false); - } - } - - Console.WriteLine(); - } - - /// - /// Prints a single dependency node on a line. - /// - /// The current package node. - /// The prefix we need to print before the current node. - /// Specifies whether the current node is the last child of its parent. - private void PrintDependencyNode(DependencyNode node, string prefix, bool isLastChild) - { - string currentPrefix, nextPrefix; - if (isLastChild) - { - currentPrefix = prefix + LastChildNodeSymbol; - nextPrefix = prefix + LastChildPrefixSymbol; - } - else - { - currentPrefix = prefix + ChildNodeSymbol; - nextPrefix = prefix + ChildPrefixSymbol; - } - - // print current node - Console.WriteLine($"{currentPrefix}{node.Id} (v{node.Version})"); - - if (node.Children?.Count > 0) - { - // recurse on the node's children - for (int i = 0; i < node.Children.Count; i++) - { - PrintDependencyNode(node.Children[i], nextPrefix, i == node.Children.Count - 1); - } - } - } - /// /// Deduplicates dependency graphs, and returns groups of frameworks that share the same graph. /// @@ -456,195 +417,157 @@ private List> GetDeduplicatedFrameworks(Dictionary - /// Returns a hash for a given dependency graph. Used to deduplicate dependency graphs for different frameworks. + /// Prints the dependency graph for a given framework/list of frameworks. /// - /// The dependency graph for a given framework. - /// - private int GetDependencyGraphHashCode(IList graph) - { - var hashCodeCombiner = new HashCodeCombiner(); - hashCodeCombiner.AddSequence(graph); - return hashCodeCombiner.CombinedHash; - } - - private class DependencyNode + /// The list of frameworks that share this dependency graph. + /// The top-level package nodes of the dependency graph. + private void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes) { - public string Id { get; set; } - public string Version { get; set; } - public IList Children { get; set; } - - // Should we distinguish between package references and project references in the output? - - // DependencyNode Parent; for stack - - public override int GetHashCode() + foreach (var framework in frameworks) { - var hashCodeCombiner = new HashCodeCombiner(); - hashCodeCombiner.AddObject(Id); - hashCodeCombiner.AddObject(Version); - hashCodeCombiner.AddSequence(Children); - return hashCodeCombiner.CombinedHash; + Console.WriteLine($"\t[{framework}]"); } - } - private List ALT_STACK_GetDependencyGraphPerFramework( - IList packageLibraries, - FrameworkPackages frameworkPackages, - string targetPackage) - { - List dependencyGraph = null; + Console.WriteLine($"\t {ChildPrefixSymbol}"); - // hashset tracking every package node that we've traversed - var visited = new HashSet(); - // dictionary tracking all package nodes that have been added to the graph, mapped to their DependencyNode objects - var addedToGraph = new Dictionary(); - // dictionary mapping all packageIds to their resolved version - Dictionary versions = GetAllResolvedVersions(packageLibraries); + if (topLevelNodes == null || topLevelNodes.Count == 0) + { + Console.WriteLine($"\t {LastChildNodeSymbol}{Strings.WhyCommand_Message_NoDependencyGraphsFoundForFramework}\n"); + return; + } - // get all the top-level packages for the framework - IEnumerable topLevelPackages = frameworkPackages.TopLevelPackages; + var stack = new Stack(); - foreach (var topLevelPackage in topLevelPackages) + // initialize the stack with all top-level nodes + for (int i = 0; i < topLevelNodes.Count; i++) { - // use depth-first search to find dependency paths from the top-level package to the target package - DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, frameworkPackages, visited, addedToGraph, versions, targetPackage); + var node = topLevelNodes[i]; - if (dependencyNode != null) + if (i == 0) { - dependencyGraph ??= []; - dependencyGraph.Add(dependencyNode); + stack.Push(new StackOutputData(node, prefix: "\t ", isLastChild: true)); + } + else + { + stack.Push(new StackOutputData(node, prefix: "\t ", isLastChild: false)); } } - return dependencyGraph; - } - - private DependencyNode ALT_STACK_FindDependencyPath( - string topLevelPackage, - IList packageLibraries, - FrameworkPackages frameworkPackages, - HashSet visited, - Dictionary addedToGraph, - Dictionary versions, - string targetPackage) - { - DependencyNode topLevelNode = null; - var stack = new Stack(); - stack.Push(new StackData(topLevelPackage, null)); - + // print the dependency graph while (stack.Count > 0) { - var currentPackage = stack.Pop(); + var current = stack.Pop(); - // if we reach the target node, return - if (currentPackage.Id == targetPackage) + string currentPrefix, nextPrefix; + if (current.IsLastChild) { - if (!addedToGraph.ContainsKey(currentPackage.Id)) - { - addedToGraph.Add(currentPackage, - new DependencyNode - { - Id = currentPackage.Id, - Version = versions[currentPackage] - }); - } - - AddToGraph(topLevelNode, currentPackage); - return addedToGraph[currentPackage]; - } - - // if we have already traversed this node's children and found dependency paths, mark it as a duplicate node and return - if (addedToGraph.ContainsKey(currentPackage)) - { - return addedToGraph[currentPackage]; + currentPrefix = current.Prefix + LastChildNodeSymbol; + nextPrefix = current.Prefix + LastChildPrefixSymbol; } - - // if we have already traversed this node's children and found no dependency paths, return null - if (visited.Contains(currentPackage)) + else { - return null; + currentPrefix = current.Prefix + ChildNodeSymbol; + nextPrefix = current.Prefix + ChildPrefixSymbol; } - visited.Add(currentPackage); + // print current node + Console.WriteLine($"{currentPrefix}{current.Node.Id} (v{current.Node.Version})"); - // find the library that matches the root package's ID, and get all its dependencies - var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackage)?.Dependencies; - - if (dependencies?.Count != 0) + if (current.Node.Children?.Count > 0) { - List paths = null; - - // recurse on the package's dependencies - foreach (var dependency in dependencies) + // push all the node's children onto the stack + int counter = 0; + foreach (var child in current.Node.Children) { - var dependencyNode = FindDependencyPath(dependency.Id, packageLibraries, frameworkPackages, visited, addedToGraph, versions, targetPackage); - - // if the dependency has a path to the target, add it to the list of paths - if (dependencyNode != null) - { - paths ??= []; - paths.Add(dependencyNode); - } - } - - // if there are any paths leading to the target, return the current node with its children - if (paths?.Count > 0) - { - if (!addedToGraph.ContainsKey(currentPackage)) - { - addedToGraph.Add(currentPackage, - new DependencyNode - { - Id = currentPackage, - Version = versions[currentPackage], - Children = paths - }); - } - - return addedToGraph[currentPackage]; + stack.Push(new StackOutputData(child, nextPrefix, isLastChild: counter++ == 0)); } } - - // if we found no paths leading to the target, return null - return null; } - return topLevelNode; + Console.WriteLine(); + } + + /// + /// Returns a hash for a given dependency graph. Used to deduplicate dependency graphs for different frameworks. + /// + /// The dependency graph for a given framework. + /// + private int GetDependencyGraphHashCode(IList graph) + { + var hashCodeCombiner = new HashCodeCombiner(); + hashCodeCombiner.AddSequence(graph); + return hashCodeCombiner.CombinedHash; } - private void AddToGraph(DependencyNode topLevelNode, StackData targetPackageInfo) + private class DependencyNode { - topLevelNode ??= new DependencyNode(); + public string Id { get; set; } + public string Version { get; set; } + public HashSet Children { get; set; } - var dependencyPath = new List(); + // TODO: Should we distinguish between package references and project references? - dependencyPath.Add(targetPackageInfo.Id); - StackData current = targetPackageInfo.Parent; + public bool Equals(DependencyNode other) + { + if (other is null) return false; - while (current != null) + return Id == other.Id && + Version == other.Version && + Children.SetEqualsWithNullCheck(other.Children); + } + + public override bool Equals(object obj) { - dependencyPath.Add(current.Id); - current = current.Parent; + return Equals(obj as DependencyNode); + } + + public static bool operator ==(DependencyNode x, DependencyNode y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + + return x.Equals(y); } - DependencyNode currentNode = topLevelNode; + public static bool operator !=(DependencyNode x, DependencyNode y) + { + return !(x == y); + } - for (int i = dependencyPath.Count - 1; i >= 0; i--) + public override int GetHashCode() { - currentNode + var hashCodeCombiner = new HashCodeCombiner(); + hashCodeCombiner.AddObject(Id); + hashCodeCombiner.AddObject(Version); + hashCodeCombiner.AddUnorderedSequence(Children); + return hashCodeCombiner.CombinedHash; } } - private class StackData + private class StackDependencyData { public string Id { get; set; } - public StackData Parent { get; set; } + public StackDependencyData Parent { get; set; } - public StackData(string currentId, StackData parent) + public StackDependencyData(string currentId, StackDependencyData parent) { Id = currentId; Parent = parent; } } + + private class StackOutputData + { + public DependencyNode Node { get; set; } + public string Prefix { get; set; } + public bool IsLastChild { get; set; } + + public StackOutputData(DependencyNode node, string prefix, bool isLastChild) + { + Node = node; + Prefix = prefix; + IsLastChild = isLastChild; + } + } } } From a44299b886cd508a117bcf7ed2f0e1fcac08c3ff Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 17 Apr 2024 20:58:21 -0700 Subject: [PATCH 15/53] wipping --- .../WhyCommand/WhyCommandRunner.cs | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index d1a707f2d93..b622705368b 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -127,7 +127,7 @@ private void FindAllDependencyGraphs( string projectName) { bool doesProjectHaveDependencyOnPackage = false; - var dependencyGraphPerFramework = new Dictionary>(targets.Count); + var dependencyGraphPerFramework = new Dictionary>(targets.Count); foreach (var frameworkPackage in frameworkPackages) { @@ -141,10 +141,10 @@ private void FindAllDependencyGraphs( // get all package libraries for the framework IList packageLibraries = target.Libraries; + // if the project has a dependency on the target package, get the dependency graph if (packageLibraries.Any(l => l.Name == targetPackage)) { doesProjectHaveDependencyOnPackage = true; - dependencyGraphPerFramework.Add(frameworkPackage.Framework, GetDependencyGraphPerFramework(frameworkPackage.TopLevelPackages, packageLibraries, targetPackage)); } @@ -182,12 +182,12 @@ private void FindAllDependencyGraphs( /// All package libraries for a given framework. /// The package we want the dependency paths for. /// - private List GetDependencyGraphPerFramework( + private HashSet GetDependencyGraphPerFramework( IEnumerable topLevelPackages, IList packageLibraries, string targetPackage) { - List dependencyGraph = null; + HashSet dependencyGraph = null; // hashset tracking every package node that we've traversed var visited = new HashSet(); @@ -199,12 +199,12 @@ private List GetDependencyGraphPerFramework( foreach (var topLevelPackage in topLevelPackages) { // use depth-first search to find dependency paths from the top-level package to the target package - DependencyNode dependencyNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, visited, dependencyNodes, versions, targetPackage); + DependencyNode topLevelNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, visited, dependencyNodes, versions, targetPackage); - if (dependencyNode != null) + if (topLevelNode != null) { dependencyGraph ??= []; - dependencyGraph.Add(dependencyNode); + dependencyGraph.Add(topLevelNode); } } @@ -256,7 +256,7 @@ private DependencyNode FindDependencyPath( var currentPackageData = stack.Pop(); var currentPackageId = currentPackageData.Id; - // if we reach the target node, or if we've already found dependency paths from the current package, add it to the graph + // if we reach the target node, or if we've already traversed this node and found dependency paths, add it to the graph if (currentPackageId == targetPackage || dependencyNodes.ContainsKey(currentPackageId)) { @@ -317,20 +317,14 @@ private void AddToGraph( current = current.Parent; } - // then, we traverse this list from the target package to the top-level package, adding/updating their dependency nodes in the graph + // then, we traverse this list from the target package to the top-level package, initializing/updating their dependency nodes as needed for (int i = 0; i < dependencyPath.Count; i++) { string currentPackageId = dependencyPath[i]; if (!dependencyNodes.ContainsKey(currentPackageId)) { - dependencyNodes.Add(currentPackageId, - new DependencyNode - { - Id = currentPackageId, - Version = versions[currentPackageId], - Children = new HashSet() - }); + dependencyNodes.Add(currentPackageId, new DependencyNode(currentPackageId, versions[currentPackageId])); } if (i > 0) @@ -340,6 +334,8 @@ private void AddToGraph( /* * Why does this not work, even though hashCodes are the same? * How can I fix the Equals override method to make this work? + */ + /* if (dependencyNodes[currentPackageId].Children.Contains(childNode)) { continue; @@ -361,7 +357,7 @@ private void AddToGraph( /// Prints the dependency graphs for all target frameworks. /// /// A dictionary mapping target frameworks to their dependency graphs. - private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) + private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) { Console.WriteLine(); @@ -381,7 +377,7 @@ private void PrintAllDependencyGraphs(Dictionary> d /// /// eg. { { "net6.0", "netcoreapp3.1" }, { "net472" } } /// - private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) + private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) { List frameworksWithoutGraphs = null; var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); @@ -421,8 +417,9 @@ private List> GetDeduplicatedFrameworks(Dictionary /// The list of frameworks that share this dependency graph. /// The top-level package nodes of the dependency graph. - private void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes) + private void PrintDependencyGraphPerFramework(List frameworks, HashSet topLevelNodes) { + // print framework header foreach (var framework in frameworks) { Console.WriteLine($"\t[{framework}]"); @@ -439,18 +436,10 @@ private void PrintDependencyGraphPerFramework(List frameworks, List(); // initialize the stack with all top-level nodes - for (int i = 0; i < topLevelNodes.Count; i++) + int counter = 0; + foreach (var node in topLevelNodes) { - var node = topLevelNodes[i]; - - if (i == 0) - { - stack.Push(new StackOutputData(node, prefix: "\t ", isLastChild: true)); - } - else - { - stack.Push(new StackOutputData(node, prefix: "\t ", isLastChild: false)); - } + stack.Push(new StackOutputData(node, prefix: "\t ", isLastChild: counter++ == 0)); } // print the dependency graph @@ -458,16 +447,16 @@ private void PrintDependencyGraphPerFramework(List frameworks, List frameworks, List 0) { // push all the node's children onto the stack - int counter = 0; + counter = 0; foreach (var child in current.Node.Children) { - stack.Push(new StackOutputData(child, nextPrefix, isLastChild: counter++ == 0)); + stack.Push(new StackOutputData(child, childPrefix, isLastChild: counter++ == 0)); } } } @@ -492,10 +481,10 @@ private void PrintDependencyGraphPerFramework(List frameworks, List /// The dependency graph for a given framework. /// - private int GetDependencyGraphHashCode(IList graph) + private int GetDependencyGraphHashCode(HashSet graph) { var hashCodeCombiner = new HashCodeCombiner(); - hashCodeCombiner.AddSequence(graph); + hashCodeCombiner.AddUnorderedSequence(graph); return hashCodeCombiner.CombinedHash; } @@ -505,6 +494,14 @@ private class DependencyNode public string Version { get; set; } public HashSet Children { get; set; } + public DependencyNode(string id, string version) + { + Id = id; + Version = version; + Children = new HashSet(); + } + + /* // TODO: Should we distinguish between package references and project references? public bool Equals(DependencyNode other) @@ -533,6 +530,7 @@ public override bool Equals(object obj) { return !(x == y); } + */ public override int GetHashCode() { @@ -549,10 +547,10 @@ private class StackDependencyData public string Id { get; set; } public StackDependencyData Parent { get; set; } - public StackDependencyData(string currentId, StackDependencyData parent) + public StackDependencyData(string currentId, StackDependencyData parentDependencyData) { Id = currentId; - Parent = parent; + Parent = parentDependencyData; } } From 073e9b593f2ebc4204e29a45aa8ea16222406d94 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 17 Apr 2024 21:00:51 -0700 Subject: [PATCH 16/53] removed TODOs to clean up --- .../WhyCommand/WhyCommandRunner.cs | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index b622705368b..f0cda46ab0e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -331,18 +331,6 @@ private void AddToGraph( { var childNode = dependencyNodes[dependencyPath[i - 1]]; - /* - * Why does this not work, even though hashCodes are the same? - * How can I fix the Equals override method to make this work? - */ - /* - if (dependencyNodes[currentPackageId].Children.Contains(childNode)) - { - continue; - } - */ - - // TODO: Replace with .Contains() once Equals override method is fixed if (dependencyNodes[currentPackageId].Children.Any(p => p.Id == childNode.Id)) { continue; @@ -501,37 +489,6 @@ public DependencyNode(string id, string version) Children = new HashSet(); } - /* - // TODO: Should we distinguish between package references and project references? - - public bool Equals(DependencyNode other) - { - if (other is null) return false; - - return Id == other.Id && - Version == other.Version && - Children.SetEqualsWithNullCheck(other.Children); - } - - public override bool Equals(object obj) - { - return Equals(obj as DependencyNode); - } - - public static bool operator ==(DependencyNode x, DependencyNode y) - { - if (ReferenceEquals(x, y)) return true; - if (x is null || y is null) return false; - - return x.Equals(y); - } - - public static bool operator !=(DependencyNode x, DependencyNode y) - { - return !(x == y); - } - */ - public override int GetHashCode() { var hashCodeCombiner = new HashCodeCombiner(); From 7ed373a3d74352892c159bab6ce0bc2a737d1ab0 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Sun, 21 Apr 2024 00:16:05 -0700 Subject: [PATCH 17/53] added basic test; failed to run basic test; --- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs new file mode 100644 index 00000000000..fc3fcc46644 --- /dev/null +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -0,0 +1,52 @@ +// 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 NuGet.CommandLine.XPlat; +using NuGet.Test.Utility; +using Xunit; + +namespace NuGet.XPlat.FuncTest +{ + public class XPlatWhyTests + { + private static readonly string ProjectName = "Test.Project.DotnetNugetWhy"; + + [Fact] + public void WhyCommand_BasicFunctionality_Succeeds() + { + // Arrange + using (var pathContext = new SimpleTestPathContext()) + { + var logger = new TestCommandOutputLogger(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + var whyCommandRunner = new WhyCommandRunner(); + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + packageY.PackageName, + [projectFramework], + logger); + + // Act + var result = whyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var output = logger.ShowMessages(); + + Assert.Equal(System.Threading.Tasks.Task.CompletedTask, result); + Assert.Equal(string.Empty, logger.ShowErrors()); + + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.PackageName}'", output); + Assert.DoesNotContain($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageY.PackageName}'", output); + } + } + } +} From aaee328d312f3312e39b399a46ee7ac0417669ba Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Mon, 22 Apr 2024 16:23:39 -0700 Subject: [PATCH 18/53] test stubs - commented out --- .../WhyCommand/WhyCommandRunner.cs | 8 ++- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index f0cda46ab0e..010b3143cc0 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -133,7 +133,7 @@ private void FindAllDependencyGraphs( { LockFileTarget target = targets.FirstOrDefault(f => f.TargetFramework.GetShortFolderName() == frameworkPackage.Framework); - if (target != null) + if (target != default) { // get all the top-level packages for the framework IEnumerable frameworkTopLevelPackages = frameworkPackage.TopLevelPackages; @@ -275,7 +275,7 @@ private DependencyNode FindDependencyPath( // get all dependencies for the current package var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackageId)?.Dependencies; - if (dependencies?.Count != 0) + if (dependencies?.Count > 0) { // push all the dependencies onto the stack foreach (var dependency in dependencies) @@ -475,7 +475,9 @@ private int GetDependencyGraphHashCode(HashSet graph) hashCodeCombiner.AddUnorderedSequence(graph); return hashCodeCombiner.CombinedHash; } - + /// + /// Represents a node in the package dependency graph. + /// private class DependencyNode { public string Id { get; set; } diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index fc3fcc46644..34c0a1b81e4 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -1,6 +1,7 @@ // 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 NuGet.CommandLine.XPlat; using NuGet.Test.Utility; using Xunit; @@ -48,5 +49,62 @@ public void WhyCommand_BasicFunctionality_Succeeds() Assert.DoesNotContain($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageY.PackageName}'", output); } } + + [Fact] + public void WhyCommand_EmptyProjectArgument_Fails() + { + // Arrange + using (var pathContext = new SimpleTestPathContext()) + { + var logger = new TestCommandOutputLogger(); + var whyCommandRunner = new WhyCommandRunner(); + var whyCommandArgs = new WhyCommandArgs( + "", + "PackageX", + [], + logger); + + // Act + var result = whyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var output = logger.ShowMessages(); + + Assert.Equal(System.Threading.Tasks.Task.CompletedTask, result); + Assert.Equal(string.Empty, logger.ShowErrors()); + + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for 'PackageX'", output); + Assert.DoesNotContain($"Project '{ProjectName}' does not have any dependency graph(s) for 'PackageX'", output); + } + } + + [Fact] + public void WhyCommand_InvalidProject_Fails() + { + // Arrange + using (var pathContext = new SimpleTestPathContext()) + { + var logger = new TestCommandOutputLogger(); + var whyCommandRunner = new WhyCommandRunner(); + var whyCommandArgs = new WhyCommandArgs( + "FakeProjectPath.csproj", + "PackageX", + [], + logger); + + // Act + var result = whyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var output = logger.ShowMessages(); + + Assert.Equal(System.Threading.Tasks.Task.CompletedTask, result); + Assert.Equal(string.Empty, logger.ShowErrors()); + + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for 'PackageX'", output); + Assert.DoesNotContain($"Project '{ProjectName}' does not have any dependency graph(s) for 'PackageX'", output); + } + } } } +*/ From ebc47321f6ce59a206c0462d7efedcd4ff525510 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Mon, 22 Apr 2024 17:46:50 -0700 Subject: [PATCH 19/53] cleanup + removed debugging code --- .../WhyCommand/WhyCommandRunner.cs | 35 +++++++++---------- .../Commands/WhyCommand.cs | 5 +-- .../NuGet.CommandLine.XPlat.csproj | 4 +-- .../NuGet.CommandLine.XPlat/Program.cs | 4 +-- .../Strings.Designer.cs | 6 ++-- .../NuGet.CommandLine.XPlat/Strings.resx | 5 +-- 6 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs index 010b3143cc0..d0282e729c3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs @@ -15,8 +15,8 @@ namespace NuGet.CommandLine.XPlat { internal class WhyCommandRunner : IWhyCommandRunner { - private const string ProjectAssetsFile = "ProjectAssetsFile"; private const string ProjectName = "MSBuildProjectName"; + private const string ProjectAssetsFile = "ProjectAssetsFile"; // Dependency graph console output symbols private const string ChildNodeSymbol = "├─── "; @@ -32,13 +32,13 @@ internal class WhyCommandRunner : IWhyCommandRunner /// public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { - var projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") - ? MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) - : new List() { whyCommandArgs.Path }; + var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); string targetPackage = whyCommandArgs.Package; - var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); + IEnumerable projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") + ? MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) + : new List() { whyCommandArgs.Path }; foreach (var projectPath in projectPaths) { @@ -102,7 +102,7 @@ public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { Console.WriteLine( string.Format( - Strings.WhyCommand_Message_NoPackagesFoundForFramework, + Strings.WhyCommand_Message_NoPackagesFoundForGivenFrameworks, projectName)); } @@ -114,7 +114,7 @@ public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) } /// - /// Runs the 'why' command, and prints out output to console. + /// Runs the 'why' command, and prints output to the console. /// /// All frameworks in the project, with their top-level packages and transitive packages. /// All lock file targets in the project. @@ -127,7 +127,7 @@ private void FindAllDependencyGraphs( string projectName) { bool doesProjectHaveDependencyOnPackage = false; - var dependencyGraphPerFramework = new Dictionary>(targets.Count); + var dependencyGraphPerFramework = new Dictionary>(targets.Count); foreach (var frameworkPackage in frameworkPackages) { @@ -135,9 +135,6 @@ private void FindAllDependencyGraphs( if (target != default) { - // get all the top-level packages for the framework - IEnumerable frameworkTopLevelPackages = frameworkPackage.TopLevelPackages; - // get all package libraries for the framework IList packageLibraries = target.Libraries; @@ -176,18 +173,19 @@ private void FindAllDependencyGraphs( } /// - /// Returns all dependency paths from the top-level packages to the target package for a given framework. + /// Finds all dependency paths from the top-level packages to the target package for a given framework. + /// Returns a set of all top-level package nodes in the dependency graph. /// /// All top-level packages for the framework. /// All package libraries for a given framework. /// The package we want the dependency paths for. /// - private HashSet GetDependencyGraphPerFramework( + private List GetDependencyGraphPerFramework( IEnumerable topLevelPackages, IList packageLibraries, string targetPackage) { - HashSet dependencyGraph = null; + List dependencyGraph = null; // hashset tracking every package node that we've traversed var visited = new HashSet(); @@ -345,7 +343,7 @@ private void AddToGraph( /// Prints the dependency graphs for all target frameworks. /// /// A dictionary mapping target frameworks to their dependency graphs. - private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) + private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) { Console.WriteLine(); @@ -365,7 +363,7 @@ private void PrintAllDependencyGraphs(Dictionary /// /// eg. { { "net6.0", "netcoreapp3.1" }, { "net472" } } /// - private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) + private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) { List frameworksWithoutGraphs = null; var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); @@ -405,7 +403,7 @@ private List> GetDeduplicatedFrameworks(Dictionary /// The list of frameworks that share this dependency graph. /// The top-level package nodes of the dependency graph. - private void PrintDependencyGraphPerFramework(List frameworks, HashSet topLevelNodes) + private void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes) { // print framework header foreach (var framework in frameworks) @@ -469,12 +467,13 @@ private void PrintDependencyGraphPerFramework(List frameworks, HashSet /// The dependency graph for a given framework. /// - private int GetDependencyGraphHashCode(HashSet graph) + private int GetDependencyGraphHashCode(List graph) { var hashCodeCombiner = new HashCodeCombiner(); hashCodeCombiner.AddUnorderedSequence(graph); return hashCodeCombiner.CombinedHash; } + /// /// Represents a node in the package dependency graph. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs index ec8bf99e229..dcbb7206e03 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs @@ -21,7 +21,7 @@ public static void Register(CommandLineApplication app, Func getLogger, why.HelpOption(XPlatUtility.HelpOption); CommandArgument path = why.Argument( - " | ", + "|", Strings.WhyCommand_PathArgument_Description, multipleValues: false); @@ -41,6 +41,7 @@ public static void Register(CommandLineApplication app, Func getLogger, ValidatePackageArgument(package); ValidateFrameworksOption(frameworks); + var whyCommandRunner = getCommandRunner(); var logger = getLogger(); var whyCommandArgs = new WhyCommandArgs( path.Value, @@ -48,8 +49,8 @@ public static void Register(CommandLineApplication app, Func getLogger, frameworks.Values, logger); - var whyCommandRunner = getCommandRunner(); whyCommandRunner.ExecuteCommand(whyCommandArgs); + return 0; }); }); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj index ad4b8e0b41f..51082266367 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj @@ -22,9 +22,9 @@ - + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index 03146ce307e..8adf56f2696 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -39,7 +39,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log) { #if DEBUG // Uncomment the following when debugging. Also uncomment the PackageReference for Microsoft.Build.Locator. - try + /*try { // .NET JIT compiles one method at a time. If this method calls `MSBuildLocator` directly, the // try block is never entered if Microsoft.Build.Locator.dll can't be found. So, run it in a @@ -50,7 +50,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log) { // MSBuildLocator is used only to enable Visual Studio debugging. // It's not needed when using a patched dotnet sdk, so it doesn't matter if it fails. - } + }*/ var debugNuGetXPlat = Environment.GetEnvironmentVariable("DEBUG_NUGET_XPLAT"); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 3f42e040837..5a29f38fa28 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -2287,11 +2287,11 @@ internal static string WhyCommand_Message_NoDependencyGraphsFoundInProject { } /// - /// Looks up a localized string similar to No packages were found for this framework.. + /// Looks up a localized string similar to No packages were found for the project '{0}' for the given framework(s).. /// - internal static string WhyCommand_Message_NoPackagesFoundForFramework { + internal static string WhyCommand_Message_NoPackagesFoundForGivenFrameworks { get { - return ResourceManager.GetString("WhyCommand_Message_NoPackagesFoundForFramework", resourceCulture); + return ResourceManager.GetString("WhyCommand_Message_NoPackagesFoundForGivenFrameworks", resourceCulture); } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 4c34feccd2a..b30820583a9 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -939,8 +939,9 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Unable to read the assets file `{0}`. {0} - Assets file path - - No packages were found for this framework. + + No packages were found for the project '{0}' for the given framework(s). + {0} - Project name Project '{0}' does not have any dependency graph(s) for '{1}' From 4a42f3e018f0aa287e77b6fb403270a42e4d9855 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Mon, 22 Apr 2024 18:21:00 -0700 Subject: [PATCH 20/53] Moved files to diff folder --- .../WhyCommand/IWhyCommandRunner.cs | 0 .../{PackageReferenceCommands => }/WhyCommand/WhyCommandArgs.cs | 0 .../{PackageReferenceCommands => }/WhyCommand/WhyCommandRunner.cs | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/{PackageReferenceCommands => }/WhyCommand/IWhyCommandRunner.cs (100%) rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/{PackageReferenceCommands => }/WhyCommand/WhyCommandArgs.cs (100%) rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/{PackageReferenceCommands => }/WhyCommand/WhyCommandRunner.cs (100%) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/IWhyCommandRunner.cs similarity index 100% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/IWhyCommandRunner.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/IWhyCommandRunner.cs diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs similarity index 100% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandArgs.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs similarity index 100% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/WhyCommand/WhyCommandRunner.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs From d9a6482e9a0a6b65e76276153916e03bdfe0bce2 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 24 Apr 2024 16:54:51 -0700 Subject: [PATCH 21/53] experimenting with tests --- .../Commands/WhyCommand/WhyCommandRunner.cs | 29 +++++++++++++++---- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 18 ++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index d0282e729c3..4cf23ea3170 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -25,6 +25,9 @@ internal class WhyCommandRunner : IWhyCommandRunner private const string ChildPrefixSymbol = "│ "; private const string LastChildPrefixSymbol = " "; + private const string BoldTextEscapeChars = "\u001b[1m"; + private const string ResetTextEscapeChars = "\u001b[0m"; + /// /// Executes the 'why' command. /// @@ -168,7 +171,7 @@ private void FindAllDependencyGraphs( projectName, targetPackage)); - PrintAllDependencyGraphs(dependencyGraphPerFramework); + PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage); } } @@ -343,7 +346,8 @@ private void AddToGraph( /// Prints the dependency graphs for all target frameworks. /// /// A dictionary mapping target frameworks to their dependency graphs. - private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework) + /// The package we want the dependency paths for. + private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework, string targetPackage) { Console.WriteLine(); @@ -352,7 +356,7 @@ private void PrintAllDependencyGraphs(Dictionary> d foreach (var frameworks in deduplicatedFrameworks) { - PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[frameworks.FirstOrDefault()]); + PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[frameworks.FirstOrDefault()], targetPackage); } } @@ -403,7 +407,8 @@ private List> GetDeduplicatedFrameworks(Dictionary /// The list of frameworks that share this dependency graph. /// The top-level package nodes of the dependency graph. - private void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes) + /// The package we want the dependency paths for. + private void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes, string targetPackage) { // print framework header foreach (var framework in frameworks) @@ -446,7 +451,21 @@ private void PrintDependencyGraphPerFramework(List frameworks, List 0) { diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index 34c0a1b81e4..d6e29126a3b 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -1,19 +1,21 @@ // 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 NuGet.CommandLine.XPlat; +using NuGet.Packaging; using NuGet.Test.Utility; using Xunit; namespace NuGet.XPlat.FuncTest { + [Collection("NuGet XPlat Test Collection")] public class XPlatWhyTests { private static readonly string ProjectName = "Test.Project.DotnetNugetWhy"; + private static MSBuildAPIUtility MsBuild => new MSBuildAPIUtility(new TestCommandOutputLogger()); [Fact] - public void WhyCommand_BasicFunctionality_Succeeds() + public async void WhyCommand_BasicFunctionality_Succeeds() { // Arrange using (var pathContext = new SimpleTestPathContext()) @@ -29,6 +31,17 @@ public void WhyCommand_BasicFunctionality_Succeeds() project.AddPackageToFramework(projectFramework, packageX); + // Generate Package + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); + var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); + var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); + var whyCommandRunner = new WhyCommandRunner(); var whyCommandArgs = new WhyCommandArgs( project.ProjectPath, @@ -107,4 +120,3 @@ public void WhyCommand_InvalidProject_Fails() } } } -*/ From 904740e5b1a4d58ad0697f9eb16c9bdce946696a Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 3 May 2024 12:23:49 -0700 Subject: [PATCH 22/53] started addressing feedback, but wow it's a lot of work --- .../Commands/WhyCommand.cs | 107 ------------------ .../Commands/WhyCommand/IWhyCommandRunner.cs | 12 -- .../Commands/WhyCommand/WhyCommand.cs | 71 ++++++++++++ .../Commands/WhyCommand/WhyCommandArgs.cs | 55 +++++++++ .../Commands/WhyCommand/WhyCommandRunner.cs | 22 ++-- .../NuGet.CommandLine.XPlat.csproj | 4 +- .../NuGet.CommandLine.XPlat/Program.cs | 15 ++- .../Strings.Designer.cs | 4 +- .../NuGet.CommandLine.XPlat/Strings.resx | 2 +- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 2 +- 10 files changed, 152 insertions(+), 142 deletions(-) delete mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs delete mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/IWhyCommandRunner.cs create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs deleted file mode 100644 index dcbb7206e03..00000000000 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs +++ /dev/null @@ -1,107 +0,0 @@ -// 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.Globalization; -using System.IO; -using System.Linq; -using Microsoft.Extensions.CommandLineUtils; -using NuGet.Common; -using NuGet.Frameworks; - -namespace NuGet.CommandLine.XPlat -{ - internal static class WhyCommand - { - public static void Register(CommandLineApplication app, Func getLogger, Func getCommandRunner) - { - app.Command("why", why => - { - why.Description = Strings.WhyCommand_Description; - why.HelpOption(XPlatUtility.HelpOption); - - CommandArgument path = why.Argument( - "|", - Strings.WhyCommand_PathArgument_Description, - multipleValues: false); - - CommandArgument package = why.Argument( - "", - Strings.WhyCommand_PackageArgument_Description, - multipleValues: false); - - CommandOption frameworks = why.Option( - "-f|--framework", - Strings.WhyCommand_FrameworkOption_Description, - CommandOptionType.MultipleValue); - - why.OnExecute(() => - { - ValidatePathArgument(path); - ValidatePackageArgument(package); - ValidateFrameworksOption(frameworks); - - var whyCommandRunner = getCommandRunner(); - var logger = getLogger(); - var whyCommandArgs = new WhyCommandArgs( - path.Value, - package.Value, - frameworks.Values, - logger); - - whyCommandRunner.ExecuteCommand(whyCommandArgs); - - return 0; - }); - }); - } - - private static void ValidatePathArgument(CommandArgument path) - { - if (string.IsNullOrEmpty(path.Value)) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - path.Name)); - } - - if (!File.Exists(path.Value) - || (!path.Value.EndsWith("proj", StringComparison.OrdinalIgnoreCase) && !path.Value.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_PathIsMissingOrInvalid, - path.Value)); - } - } - - private static void ValidatePackageArgument(CommandArgument package) - { - if (string.IsNullOrEmpty(package.Value)) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - package.Name)); - } - } - - private static void ValidateFrameworksOption(CommandOption framework) - { - var frameworks = framework.Values.Select(f => - NuGetFramework.Parse( - f.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .ToArray()[0])); - - if (frameworks.Any(f => f.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_InvalidFramework, - framework.Template)); - } - } - } -} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/IWhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/IWhyCommandRunner.cs deleted file mode 100644 index 8815b00631f..00000000000 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/IWhyCommandRunner.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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.Threading.Tasks; - -namespace NuGet.CommandLine.XPlat -{ - internal interface IWhyCommandRunner - { - Task ExecuteCommand(WhyCommandArgs whyCommandArgs); - } -} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs new file mode 100644 index 00000000000..5cec3ce5b5a --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs @@ -0,0 +1,71 @@ +// 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.CommandLine; +using System.CommandLine.Help; + +namespace NuGet.CommandLine.XPlat +{ + internal static class WhyCommand + { + public static void Register(CliCommand rootCommand, Func getLogger) + { + var whyCommand = new CliCommand("why", Strings.WhyCommand_Description); + + var path = new CliArgument("|") + { + Description = Strings.WhyCommand_PathArgument_Description, + Arity = ArgumentArity.ExactlyOne // ZeroOrOne + }; + + var package = new CliArgument("PACKAGE_NAME") + { + Description = Strings.WhyCommand_PackageArgument_Description, + Arity = ArgumentArity.ExactlyOne // ZeroOrOne + }; + + var frameworks = new CliOption>("-f|--framework") + { + Description = Strings.WhyCommand_FrameworksOption_Description, + Arity = ArgumentArity.OneOrMore + }; + + var help = new HelpOption() + { + Arity = ArgumentArity.Zero + }; + + whyCommand.Arguments.Add(path); + whyCommand.Arguments.Add(package); + whyCommand.Options.Add(frameworks); + whyCommand.Options.Add(help); + + whyCommand.SetAction((parseResult) => + { + ILoggerWithColor logger = getLogger(); + + try + { + var whyCommandArgs = new WhyCommandArgs( + parseResult.GetValue(path), + parseResult.GetValue(package), + parseResult.GetValue(frameworks), + logger); + + WhyCommandRunner.ExecuteCommand(whyCommandArgs); + + return 0; + } + catch (ArgumentException ex) + { + logger.LogError(ex.Message); + return ExitCodes.InvalidArguments; + } + }); + + rootCommand.Subcommands.Add(whyCommand); + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs index d3028b03acc..b3cabfba934 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs @@ -3,7 +3,11 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; using NuGet.Common; +using NuGet.Frameworks; namespace NuGet.CommandLine.XPlat { @@ -31,6 +35,57 @@ public WhyCommandArgs( Package = package ?? throw new ArgumentNullException(nameof(package)); Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + ValidatePathArgument(Path); + ValidatePackageArgument(Package); + ValidateFrameworksOption(Frameworks); + } + + private static void ValidatePathArgument(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + "|")); + } + + if (!File.Exists(path) + || (!path.EndsWith("proj", StringComparison.OrdinalIgnoreCase) && !path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_PathIsMissingOrInvalid, + path)); + } + } + + private static void ValidatePackageArgument(string package) + { + if (string.IsNullOrEmpty(package)) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + "")); + } + } + + private static void ValidateFrameworksOption(List frameworks) + { + var parsedFrameworks = frameworks.Select(f => + NuGetFramework.Parse( + f.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .ToArray()[0])); + + if (parsedFrameworks.Any(f => f.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_InvalidFramework)); + } } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 4cf23ea3170..04b081294d5 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -13,7 +13,7 @@ namespace NuGet.CommandLine.XPlat { - internal class WhyCommandRunner : IWhyCommandRunner + internal static class WhyCommandRunner { private const string ProjectName = "MSBuildProjectName"; private const string ProjectAssetsFile = "ProjectAssetsFile"; @@ -33,7 +33,7 @@ internal class WhyCommandRunner : IWhyCommandRunner /// /// CLI arguments for the 'why' command. /// - public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) + public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); @@ -123,7 +123,7 @@ public Task ExecuteCommand(WhyCommandArgs whyCommandArgs) /// All lock file targets in the project. /// The package we want the dependency paths for. /// The name of the current project. - private void FindAllDependencyGraphs( + private static void FindAllDependencyGraphs( IEnumerable frameworkPackages, IList targets, string targetPackage, @@ -183,7 +183,7 @@ private void FindAllDependencyGraphs( /// All package libraries for a given framework. /// The package we want the dependency paths for. /// - private List GetDependencyGraphPerFramework( + private static List GetDependencyGraphPerFramework( IEnumerable topLevelPackages, IList packageLibraries, string targetPackage) @@ -218,7 +218,7 @@ private List GetDependencyGraphPerFramework( /// /// All package libraries for a given framework. /// - private Dictionary GetAllResolvedVersions(IList packageLibraries) + private static Dictionary GetAllResolvedVersions(IList packageLibraries) { var versions = new Dictionary(); @@ -241,7 +241,7 @@ private Dictionary GetAllResolvedVersions(IListDictionary mapping packageIds to their resolved versions. /// The package we want the dependency paths for. /// - private DependencyNode FindDependencyPath( + private static DependencyNode FindDependencyPath( string topLevelPackage, IList packageLibraries, HashSet visited, @@ -303,7 +303,7 @@ private DependencyNode FindDependencyPath( /// up to the top-level package. /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. /// Dictionary mapping packageIds to their resolved versions. - private void AddToGraph( + private static void AddToGraph( StackDependencyData targetPackageData, Dictionary dependencyNodes, Dictionary versions) @@ -347,7 +347,7 @@ private void AddToGraph( /// /// A dictionary mapping target frameworks to their dependency graphs. /// The package we want the dependency paths for. - private void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework, string targetPackage) + private static void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework, string targetPackage) { Console.WriteLine(); @@ -367,7 +367,7 @@ private void PrintAllDependencyGraphs(Dictionary> d /// /// eg. { { "net6.0", "netcoreapp3.1" }, { "net472" } } /// - private List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) + private static List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) { List frameworksWithoutGraphs = null; var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); @@ -408,7 +408,7 @@ private List> GetDeduplicatedFrameworks(DictionaryThe list of frameworks that share this dependency graph. /// The top-level package nodes of the dependency graph. /// The package we want the dependency paths for. - private void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes, string targetPackage) + private static void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes, string targetPackage) { // print framework header foreach (var framework in frameworks) @@ -486,7 +486,7 @@ private void PrintDependencyGraphPerFramework(List frameworks, List /// The dependency graph for a given framework. /// - private int GetDependencyGraphHashCode(List graph) + private static int GetDependencyGraphHashCode(List graph) { var hashCodeCombiner = new HashCodeCombiner(); hashCodeCombiner.AddUnorderedSequence(graph); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj index 51082266367..ad4b8e0b41f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj @@ -22,9 +22,9 @@ - + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index 8adf56f2696..f28b42a762d 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -39,7 +39,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log) { #if DEBUG // Uncomment the following when debugging. Also uncomment the PackageReference for Microsoft.Build.Locator. - /*try + try { // .NET JIT compiles one method at a time. If this method calls `MSBuildLocator` directly, the // try block is never entered if Microsoft.Build.Locator.dll can't be found. So, run it in a @@ -50,7 +50,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log) { // MSBuildLocator is used only to enable Visual Studio debugging. // It's not needed when using a patched dotnet sdk, so it doesn't matter if it fails. - }*/ + } var debugNuGetXPlat = Environment.GetEnvironmentVariable("DEBUG_NUGET_XPLAT"); @@ -74,9 +74,11 @@ public static int MainInternal(string[] args, CommandOutputLogger log) NuGet.Common.Migrations.MigrationRunner.Run(); - if ((args.Count() >= 2 && args[0] == "package" && args[1] == "search") || (args.Any() && args[0] == "config")) + // Migrating from Microsoft.Extensions.CommandLineUtils.CommandLineApplication to System.Commandline.CliCommand + if ((args.Count() >= 2 && args[0] == "package" && args[1] == "search") + || (args.Any() && args[0] == "config") + || (args.Any() && args[0] == "why")) { - // We are executing command `dotnet package search` Func getHidePrefixLogger = () => { log.HidePrefixForInfoAndMinimal = true; @@ -84,14 +86,16 @@ public static int MainInternal(string[] args, CommandOutputLogger log) }; CliCommand rootCommand = new CliCommand("package"); + PackageSearchCommand.Register(rootCommand, getHidePrefixLogger); ConfigCommand.Register(rootCommand, getHidePrefixLogger); + WhyCommand.Register(rootCommand, getHidePrefixLogger); CancellationTokenSource tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(TimeSpan.FromMinutes(DotnetPackageSearchTimeOut)); - int exitCodeValue = 0; CliConfiguration config = new(rootCommand); ParseResult parseResult = rootCommand.Parse(args, config); + int exitCodeValue = 0; try { @@ -255,7 +259,6 @@ private static CommandLineApplication InitializeApp(string[] args, CommandOutput VerifyCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new VerifyCommandRunner()); TrustedSignersCommand.Register(app, getHidePrefixLogger, setLogLevel); SignCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new SignCommandRunner()); - WhyCommand.Register(app, getHidePrefixLogger, () => new WhyCommandRunner()); } app.FullName = Strings.App_FullName; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 5a29f38fa28..a74495fe2c3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -2253,9 +2253,9 @@ internal static string WhyCommand_Error_PathIsMissingOrInvalid { /// /// Looks up a localized string similar to The target framework(s) for which dependency graphs are shown.. /// - internal static string WhyCommand_FrameworkOption_Description { + internal static string WhyCommand_FrameworksOption_Description { get { - return ResourceManager.GetString("WhyCommand_FrameworkOption_Description", resourceCulture); + return ResourceManager.GetString("WhyCommand_FrameworksOption_Description", resourceCulture); } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index b30820583a9..19f26a794be 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -921,7 +921,7 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT The package name to look-up in the dependency graph. - + The target framework(s) for which dependency graphs are shown. diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index d6e29126a3b..22bc66d9579 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -45,7 +45,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var whyCommandRunner = new WhyCommandRunner(); var whyCommandArgs = new WhyCommandArgs( project.ProjectPath, - packageY.PackageName, + packageY.Id, [projectFramework], logger); From 69bfb3bd1d61eb8d1662f5d7205e2f7648b62e35 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 3 May 2024 12:45:24 -0700 Subject: [PATCH 23/53] adjusted tests to static class --- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index 22bc66d9579..cd6b9d256cc 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -42,7 +42,6 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); - var whyCommandRunner = new WhyCommandRunner(); var whyCommandArgs = new WhyCommandArgs( project.ProjectPath, packageY.Id, @@ -50,7 +49,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( logger); // Act - var result = whyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var output = logger.ShowMessages(); @@ -70,7 +69,6 @@ public void WhyCommand_EmptyProjectArgument_Fails() using (var pathContext = new SimpleTestPathContext()) { var logger = new TestCommandOutputLogger(); - var whyCommandRunner = new WhyCommandRunner(); var whyCommandArgs = new WhyCommandArgs( "", "PackageX", @@ -78,7 +76,7 @@ public void WhyCommand_EmptyProjectArgument_Fails() logger); // Act - var result = whyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var output = logger.ShowMessages(); @@ -98,7 +96,6 @@ public void WhyCommand_InvalidProject_Fails() using (var pathContext = new SimpleTestPathContext()) { var logger = new TestCommandOutputLogger(); - var whyCommandRunner = new WhyCommandRunner(); var whyCommandArgs = new WhyCommandArgs( "FakeProjectPath.csproj", "PackageX", @@ -106,7 +103,7 @@ public void WhyCommand_InvalidProject_Fails() logger); // Act - var result = whyCommandRunner.ExecuteCommand(whyCommandArgs); + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert var output = logger.ShowMessages(); From cb0c0437792e75303b70cf1a7026e979c2000490 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Tue, 7 May 2024 08:43:35 -0700 Subject: [PATCH 24/53] wip --- .../Commands/WhyCommand/WhyCommand.cs | 56 ++--- .../Commands/WhyCommand/WhyCommandArgs.cs | 12 +- .../Commands/WhyCommand/WhyCommandRunner.cs | 234 +++++++++--------- .../NuGet.CommandLine.XPlat/Program.cs | 8 + .../Strings.Designer.cs | 10 +- .../NuGet.CommandLine.XPlat/Strings.resx | 10 +- 6 files changed, 175 insertions(+), 155 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs index 5cec3ce5b5a..51a95ce6ff2 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs @@ -10,37 +10,37 @@ namespace NuGet.CommandLine.XPlat { internal static class WhyCommand { - public static void Register(CliCommand rootCommand, Func getLogger) + private static CliArgument Path = new CliArgument("PROJECT|SOLUTION") { - var whyCommand = new CliCommand("why", Strings.WhyCommand_Description); + Description = Strings.WhyCommand_PathArgument_Description, + Arity = ArgumentArity.ExactlyOne // ZeroOrOne? + }; - var path = new CliArgument("|") - { - Description = Strings.WhyCommand_PathArgument_Description, - Arity = ArgumentArity.ExactlyOne // ZeroOrOne - }; + private static CliArgument Package = new CliArgument("PACKAGE") + { + Description = Strings.WhyCommand_PackageArgument_Description, + Arity = ArgumentArity.ExactlyOne + }; - var package = new CliArgument("PACKAGE_NAME") - { - Description = Strings.WhyCommand_PackageArgument_Description, - Arity = ArgumentArity.ExactlyOne // ZeroOrOne - }; + private static CliOption> Frameworks = new CliOption>("-f|--framework") + { + Description = Strings.WhyCommand_FrameworksOption_Description, + Arity = ArgumentArity.OneOrMore + }; - var frameworks = new CliOption>("-f|--framework") - { - Description = Strings.WhyCommand_FrameworksOption_Description, - Arity = ArgumentArity.OneOrMore - }; + private static HelpOption Help = new HelpOption() + { + Arity = ArgumentArity.Zero + }; - var help = new HelpOption() - { - Arity = ArgumentArity.Zero - }; + internal static void Register(CliCommand rootCommand, Func getLogger) + { + var whyCommand = new CliCommand("why", Strings.WhyCommand_Description); - whyCommand.Arguments.Add(path); - whyCommand.Arguments.Add(package); - whyCommand.Options.Add(frameworks); - whyCommand.Options.Add(help); + whyCommand.Arguments.Add(Path); + whyCommand.Arguments.Add(Package); + whyCommand.Options.Add(Frameworks); + whyCommand.Options.Add(Help); whyCommand.SetAction((parseResult) => { @@ -49,9 +49,9 @@ public static void Register(CliCommand rootCommand, Func getLo try { var whyCommandArgs = new WhyCommandArgs( - parseResult.GetValue(path), - parseResult.GetValue(package), - parseResult.GetValue(frameworks), + parseResult.GetValue(Path), + parseResult.GetValue(Package), + parseResult.GetValue(Frameworks), logger); WhyCommandRunner.ExecuteCommand(whyCommandArgs); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs index b3cabfba934..61735e54193 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs @@ -6,17 +6,16 @@ using System.Globalization; using System.IO; using System.Linq; -using NuGet.Common; using NuGet.Frameworks; namespace NuGet.CommandLine.XPlat { internal class WhyCommandArgs { - public ILogger Logger { get; } public string Path { get; } public string Package { get; } public List Frameworks { get; } + public ILoggerWithColor Logger { get; } /// /// A constructor for the arguments of the 'why' command. @@ -29,7 +28,7 @@ public WhyCommandArgs( string path, string package, List frameworks, - ILogger logger) + ILoggerWithColor logger) { Path = path ?? throw new ArgumentNullException(nameof(path)); Package = package ?? throw new ArgumentNullException(nameof(package)); @@ -48,11 +47,12 @@ private static void ValidatePathArgument(string path) throw new ArgumentException( string.Format(CultureInfo.CurrentCulture, Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - "|")); + "PROJECT|SOLUTION")); } if (!File.Exists(path) - || (!path.EndsWith("proj", StringComparison.OrdinalIgnoreCase) && !path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) + || (!path.EndsWith("proj", StringComparison.OrdinalIgnoreCase) + && !path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException( string.Format(CultureInfo.CurrentCulture, @@ -68,7 +68,7 @@ private static void ValidatePackageArgument(string package) throw new ArgumentException( string.Format(CultureInfo.CurrentCulture, Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - "")); + "PACKAGE")); } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 04b081294d5..d2b9545c95f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -19,20 +19,16 @@ internal static class WhyCommandRunner private const string ProjectAssetsFile = "ProjectAssetsFile"; // Dependency graph console output symbols - private const string ChildNodeSymbol = "├─── "; - private const string LastChildNodeSymbol = "└─── "; + private const string ChildNodeSymbol = "├─ "; + private const string LastChildNodeSymbol = "└─ "; - private const string ChildPrefixSymbol = "│ "; - private const string LastChildPrefixSymbol = " "; - - private const string BoldTextEscapeChars = "\u001b[1m"; - private const string ResetTextEscapeChars = "\u001b[0m"; + private const string ChildPrefixSymbol = "│ "; + private const string LastChildPrefixSymbol = " "; /// /// Executes the 'why' command. /// /// CLI arguments for the 'why' command. - /// public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) { var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); @@ -41,148 +37,171 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) IEnumerable projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") ? MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) - : new List() { whyCommandArgs.Path }; + : [whyCommandArgs.Path]; foreach (var projectPath in projectPaths) { Project project = MSBuildAPIUtility.GetProject(projectPath); + LockFile assetsFile = GetProjectAssetsFile(project, whyCommandArgs); - // if the current project is not a PackageReference project, print an error message and continue to the next project - if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) + if (assetsFile != null) { - Console.Error.WriteLine( - string.Format( - CultureInfo.CurrentCulture, - Strings.Error_NotPRProject, - projectPath)); - - ProjectCollection.GlobalProjectCollection.UnloadProject(project); - continue; + FindAllDependencyGraphs(whyCommandArgs, project, assetsFile); } - string projectName = project.GetPropertyValue(ProjectName); - string assetsPath = project.GetPropertyValue(ProjectAssetsFile); + ProjectCollection.GlobalProjectCollection.UnloadProject(project); + } - // if the assets file was not found, print an error message and continue to the next project - if (!File.Exists(assetsPath)) - { - Console.Error.WriteLine( - string.Format( - CultureInfo.CurrentCulture, - Strings.Error_AssetsFileNotFound, - projectPath)); + return Task.CompletedTask; + } - ProjectCollection.GlobalProjectCollection.UnloadProject(project); - continue; - } + /// + /// Validates and returns the assets file for the given project. + /// + /// Evaluated MSBuild project + /// Assets file for given project + private static LockFile GetProjectAssetsFile(Project project, WhyCommandArgs whyCommandArgs) + { + if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) + { + /* + Console.Error.WriteLine( + string.Format( + CultureInfo.CurrentCulture, + Strings.Error_NotPRProject, + project.FullPath)); + */ - var lockFileFormat = new LockFileFormat(); - LockFile assetsFile = lockFileFormat.Read(assetsPath); + whyCommandArgs.Logger.LogError( + string.Format( + CultureInfo.CurrentCulture, + Strings.Error_NotPRProject, + project.FullPath)); - // assets file validation - if (assetsFile.PackageSpec == null - || assetsFile.Targets == null - || assetsFile.Targets.Count == 0) - { - Console.Error.WriteLine( - string.Format( - CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_CannotReadAssetsFile, - assetsPath)); + return null; + } - ProjectCollection.GlobalProjectCollection.UnloadProject(project); - continue; - } + string assetsPath = project.GetPropertyValue(ProjectAssetsFile); - // get all resolved package references for a project - List packages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true); + if (!File.Exists(assetsPath)) + { + /* + Console.Error.WriteLine( + string.Format( + CultureInfo.CurrentCulture, + Strings.Error_AssetsFileNotFound, + project.FullPath)); + */ - if (packages?.Count > 0) - { - FindAllDependencyGraphs(packages, assetsFile.Targets, targetPackage, projectName); - } - else - { - Console.WriteLine( - string.Format( - Strings.WhyCommand_Message_NoPackagesFoundForGivenFrameworks, - projectName)); - } + whyCommandArgs.Logger.LogError( + string.Format( + CultureInfo.CurrentCulture, + Strings.Error_AssetsFileNotFound, + project.FullPath)); - // unload project - ProjectCollection.GlobalProjectCollection.UnloadProject(project); + return null; } - return Task.CompletedTask; + var lockFileFormat = new LockFileFormat(); + LockFile assetsFile = lockFileFormat.Read(assetsPath); + + // assets file validation + if (assetsFile.PackageSpec == null + || assetsFile.Targets == null + || assetsFile.Targets.Count == 0) + { + Console.Error.WriteLine( + string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_CannotReadAssetsFile, + assetsPath)); + + return null; + } + + return assetsFile; } /// /// Runs the 'why' command, and prints output to the console. /// - /// All frameworks in the project, with their top-level packages and transitive packages. - /// All lock file targets in the project. - /// The package we want the dependency paths for. - /// The name of the current project. + /// CLI arguments for the 'why' command + /// Current project + /// Assets file for current project private static void FindAllDependencyGraphs( - IEnumerable frameworkPackages, - IList targets, - string targetPackage, - string projectName) + WhyCommandArgs whyCommandArgs, + Project project, + LockFile assetsFile) { - bool doesProjectHaveDependencyOnPackage = false; - var dependencyGraphPerFramework = new Dictionary>(targets.Count); + var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); // is it bad to do this repeatedly per project? + + // get all resolved package references for a project + List frameworkPackages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true); - foreach (var frameworkPackage in frameworkPackages) + if (frameworkPackages?.Count > 0) { - LockFileTarget target = targets.FirstOrDefault(f => f.TargetFramework.GetShortFolderName() == frameworkPackage.Framework); + string targetPackage = whyCommandArgs.Package; + bool doesProjectHaveDependencyOnPackage = false; + var dependencyGraphPerFramework = new Dictionary>(assetsFile.Targets.Count); - if (target != default) + foreach (var frameworkPackage in frameworkPackages) { - // get all package libraries for the framework - IList packageLibraries = target.Libraries; + LockFileTarget target = assetsFile.GetTarget(frameworkPackage.Framework, runtimeIdentifier: null); // runtime Identifier? - // if the project has a dependency on the target package, get the dependency graph - if (packageLibraries.Any(l => l.Name == targetPackage)) - { - doesProjectHaveDependencyOnPackage = true; - dependencyGraphPerFramework.Add(frameworkPackage.Framework, - GetDependencyGraphPerFramework(frameworkPackage.TopLevelPackages, packageLibraries, targetPackage)); - } - else + if (target != default) { - dependencyGraphPerFramework.Add(frameworkPackage.Framework, null); + // get all package libraries for the framework + IList packageLibraries = target.Libraries; + + // if the project has a dependency on the target package, get the dependency graph + if (packageLibraries.Any(l => l.Name == targetPackage)) + { + doesProjectHaveDependencyOnPackage = true; + dependencyGraphPerFramework.Add(frameworkPackage.Framework, + GetDependencyGraphPerFramework(frameworkPackage.TopLevelPackages, packageLibraries, targetPackage)); + } + else + { + dependencyGraphPerFramework.Add(frameworkPackage.Framework, null); + } } } - } - if (!doesProjectHaveDependencyOnPackage) - { - Console.WriteLine( - string.Format( - Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, - projectName, - targetPackage)); + if (!doesProjectHaveDependencyOnPackage) + { + Console.WriteLine( + string.Format( + Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, + project.GetPropertyValue(ProjectName), + targetPackage)); + } + else + { + Console.WriteLine( + string.Format( + Strings.WhyCommand_Message_DependencyGraphsFoundInProject, + project.GetPropertyValue(ProjectName), + targetPackage)); + + PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage); + } } else { Console.WriteLine( string.Format( - Strings.WhyCommand_Message_DependencyGraphsFoundInProject, - projectName, - targetPackage)); - - PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage); + Strings.WhyCommand_Message_NoPackagesFoundForGivenFrameworks, + project.GetPropertyValue(ProjectName))); } } /// /// Finds all dependency paths from the top-level packages to the target package for a given framework. - /// Returns a set of all top-level package nodes in the dependency graph. /// /// All top-level packages for the framework. /// All package libraries for a given framework. /// The package we want the dependency paths for. - /// + /// List of all top-level package nodes in the dependency graph. private static List GetDependencyGraphPerFramework( IEnumerable topLevelPackages, IList packageLibraries, @@ -217,7 +236,6 @@ private static List GetDependencyGraphPerFramework( /// Adds all resolved versions of packages to a dictionary. /// /// All package libraries for a given framework. - /// private static Dictionary GetAllResolvedVersions(IList packageLibraries) { var versions = new Dictionary(); @@ -232,7 +250,6 @@ private static Dictionary GetAllResolvedVersions(IList /// Traverses the dependency graph for a given top-level package, looking for a path to the target package. - /// Returns the top-level package node if a path was found, and null if no path was found. /// /// Top-level package. /// All package libraries for a given framework. @@ -240,7 +257,7 @@ private static Dictionary GetAllResolvedVersions(IListDictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. /// Dictionary mapping packageIds to their resolved versions. /// The package we want the dependency paths for. - /// + /// The top-level package node in the dependency graph (if a path was found), or null (if no path was found) private static DependencyNode FindDependencyPath( string topLevelPackage, IList packageLibraries, @@ -453,14 +470,10 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li // print current node if (current.Node.Id == targetPackage) { - /* Console.Write($"{currentPrefix}"); - Console.ForegroundColor = ConsoleColor.Red; + Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine($"{current.Node.Id} (v{current.Node.Version})"); Console.ResetColor(); - */ - - Console.WriteLine($"{currentPrefix}{BoldTextEscapeChars}{current.Node.Id} (v{current.Node.Version}){ResetTextEscapeChars}"); } else { @@ -485,7 +498,6 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li /// Returns a hash for a given dependency graph. Used to deduplicate dependency graphs for different frameworks. /// /// The dependency graph for a given framework. - /// private static int GetDependencyGraphHashCode(List graph) { var hashCodeCombiner = new HashCodeCombiner(); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index f28b42a762d..5e86a527c1a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -87,9 +87,17 @@ public static int MainInternal(string[] args, CommandOutputLogger log) CliCommand rootCommand = new CliCommand("package"); + if (args[0] == "why") + { + rootCommand = new CliCommand("nuget"); + } + PackageSearchCommand.Register(rootCommand, getHidePrefixLogger); ConfigCommand.Register(rootCommand, getHidePrefixLogger); WhyCommand.Register(rootCommand, getHidePrefixLogger); + // this won't show up in help for dotnet nuget * this way + // eg. `dotnet nuget blah` should show the help option for `dotnet nuget`, which should list all sub commands, + // but it won't enter this if block, so we won't see 'why' as a subcommand CancellationTokenSource tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(TimeSpan.FromMinutes(DotnetPackageSearchTimeOut)); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index a74495fe2c3..b3343d1b30c 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -2233,7 +2233,7 @@ internal static string WhyCommand_Error_CannotReadAssetsFile { } /// - /// Looks up a localized string similar to Failed to parse one of the given frameworks. Please make sure the given frameworks are valid.. + /// Looks up a localized string similar to Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. For a list of accepted values, please visit https://learn.microsoft.com/en-us/nuget/reference/target-frameworks#supported-frameworks.. /// internal static string WhyCommand_Error_InvalidFramework { get { @@ -2269,7 +2269,7 @@ internal static string WhyCommand_Message_DependencyGraphsFoundInProject { } /// - /// Looks up a localized string similar to No dependency graph(s) found. + /// Looks up a localized string similar to No dependency graph(s) found.. /// internal static string WhyCommand_Message_NoDependencyGraphsFoundForFramework { get { @@ -2278,7 +2278,7 @@ internal static string WhyCommand_Message_NoDependencyGraphsFoundForFramework { } /// - /// Looks up a localized string similar to Project '{0}' does not have any dependency graph(s) for '{1}'. + /// Looks up a localized string similar to Project '{0}' does not have any dependency graph(s) for '{1}'.. /// internal static string WhyCommand_Message_NoDependencyGraphsFoundInProject { get { @@ -2296,7 +2296,7 @@ internal static string WhyCommand_Message_NoPackagesFoundForGivenFrameworks { } /// - /// Looks up a localized string similar to The package name to look-up in the dependency graph.. + /// Looks up a localized string similar to The package name to lookup in the dependency graph.. /// internal static string WhyCommand_PackageArgument_Description { get { @@ -2305,7 +2305,7 @@ internal static string WhyCommand_PackageArgument_Description { } /// - /// Looks up a localized string similar to A path to a project, solution file or directory.. + /// Looks up a localized string similar to A path to a project, solution file, or directory.. /// internal static string WhyCommand_PathArgument_Description { get { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 19f26a794be..10fe3061a14 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -916,10 +916,10 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Lists the dependency graph for a particular package for a given project or solution. - A path to a project, solution file or directory. + A path to a project, solution file, or directory. - The package name to look-up in the dependency graph. + The package name to lookup in the dependency graph. The target framework(s) for which dependency graphs are shown. @@ -933,7 +933,7 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT {0} - Project/solution file path - Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. + Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. For a list of accepted values, please visit https://learn.microsoft.com/en-us/nuget/reference/target-frameworks#supported-frameworks. Unable to read the assets file `{0}`. @@ -944,7 +944,7 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT {0} - Project name - Project '{0}' does not have any dependency graph(s) for '{1}' + Project '{0}' does not have any dependency graph(s) for '{1}'. {0} - Project name, {1} - Target package @@ -952,6 +952,6 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT {0} - Project name, {1} - Target package - No dependency graph(s) found + No dependency graph(s) found. From e55583aa549d3a5446da9bf2c7ee245bf21e2095 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 8 May 2024 11:30:02 -0700 Subject: [PATCH 25/53] nit --- .../Commands/WhyCommand/WhyCommandRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index d2b9545c95f..65064b1ed56 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -123,7 +123,7 @@ private static LockFile GetProjectAssetsFile(Project project, WhyCommandArgs why } /// - /// Runs the 'why' command, and prints output to the console. + /// Finds dependency graphs for a given project, and prints output to the console. /// /// CLI arguments for the 'why' command /// Current project From 7891af4cd705f85094c515dc13f0bf8184d95dae Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 9 May 2024 12:33:00 -0700 Subject: [PATCH 26/53] temp: working on project refs --- .../Commands/WhyCommand/WhyCommandRunner.cs | 73 ++++++++----------- .../Utility/CommandOutputLogger.cs | 2 +- .../Utility/MSBuildAPIUtility.cs | 48 +++++++++++- .../TestCommandOutputLogger.cs | 5 ++ .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 23 +++++- 5 files changed, 101 insertions(+), 50 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 65064b1ed56..245bafa5b1a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -18,6 +18,8 @@ internal static class WhyCommandRunner private const string ProjectName = "MSBuildProjectName"; private const string ProjectAssetsFile = "ProjectAssetsFile"; + private const ConsoleColor TargetPackageColor = ConsoleColor.Cyan; + // Dependency graph console output symbols private const string ChildNodeSymbol = "├─ "; private const string LastChildNodeSymbol = "└─ "; @@ -42,11 +44,11 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) foreach (var projectPath in projectPaths) { Project project = MSBuildAPIUtility.GetProject(projectPath); - LockFile assetsFile = GetProjectAssetsFile(project, whyCommandArgs); + LockFile assetsFile = GetProjectAssetsFile(project, whyCommandArgs.Logger); if (assetsFile != null) { - FindAllDependencyGraphs(whyCommandArgs, project, assetsFile); + FindAllDependencyGraphs(whyCommandArgs, msBuild, project, assetsFile); } ProjectCollection.GlobalProjectCollection.UnloadProject(project); @@ -59,20 +61,13 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) /// Validates and returns the assets file for the given project. /// /// Evaluated MSBuild project + /// Logger for the 'why' command /// Assets file for given project - private static LockFile GetProjectAssetsFile(Project project, WhyCommandArgs whyCommandArgs) + private static LockFile GetProjectAssetsFile(Project project, ILoggerWithColor logger) { if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) { - /* - Console.Error.WriteLine( - string.Format( - CultureInfo.CurrentCulture, - Strings.Error_NotPRProject, - project.FullPath)); - */ - - whyCommandArgs.Logger.LogError( + logger.LogError( string.Format( CultureInfo.CurrentCulture, Strings.Error_NotPRProject, @@ -85,15 +80,7 @@ private static LockFile GetProjectAssetsFile(Project project, WhyCommandArgs why if (!File.Exists(assetsPath)) { - /* - Console.Error.WriteLine( - string.Format( - CultureInfo.CurrentCulture, - Strings.Error_AssetsFileNotFound, - project.FullPath)); - */ - - whyCommandArgs.Logger.LogError( + logger.LogError( string.Format( CultureInfo.CurrentCulture, Strings.Error_AssetsFileNotFound, @@ -110,7 +97,7 @@ private static LockFile GetProjectAssetsFile(Project project, WhyCommandArgs why || assetsFile.Targets == null || assetsFile.Targets.Count == 0) { - Console.Error.WriteLine( + logger.LogError( string.Format( CultureInfo.CurrentCulture, Strings.WhyCommand_Error_CannotReadAssetsFile, @@ -126,17 +113,18 @@ private static LockFile GetProjectAssetsFile(Project project, WhyCommandArgs why /// Finds dependency graphs for a given project, and prints output to the console. /// /// CLI arguments for the 'why' command + /// MSBuild utility /// Current project /// Assets file for current project private static void FindAllDependencyGraphs( WhyCommandArgs whyCommandArgs, + MSBuildAPIUtility msBuild, Project project, LockFile assetsFile) { - var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); // is it bad to do this repeatedly per project? - + // TODO: The top-level packages here are only package references, and do not include project references. Need to fix this. // get all resolved package references for a project - List frameworkPackages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true); + List frameworkPackages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true, includeProjectReferences: true); if (frameworkPackages?.Count > 0) { @@ -169,7 +157,7 @@ private static void FindAllDependencyGraphs( if (!doesProjectHaveDependencyOnPackage) { - Console.WriteLine( + whyCommandArgs.Logger.LogMinimal( string.Format( Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, project.GetPropertyValue(ProjectName), @@ -177,19 +165,19 @@ private static void FindAllDependencyGraphs( } else { - Console.WriteLine( + whyCommandArgs.Logger.LogMinimal( string.Format( Strings.WhyCommand_Message_DependencyGraphsFoundInProject, project.GetPropertyValue(ProjectName), targetPackage)); - PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage); + PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); } } else { - Console.WriteLine( - string.Format( + whyCommandArgs.Logger.LogMinimal( + string.Format( Strings.WhyCommand_Message_NoPackagesFoundForGivenFrameworks, project.GetPropertyValue(ProjectName))); } @@ -364,16 +352,17 @@ private static void AddToGraph( /// /// A dictionary mapping target frameworks to their dependency graphs. /// The package we want the dependency paths for. - private static void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework, string targetPackage) + private static void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework, string targetPackage, ILoggerWithColor logger) { - Console.WriteLine(); + // print empty line + logger.LogMinimal(""); // deduplicate the dependency graphs List> deduplicatedFrameworks = GetDeduplicatedFrameworks(dependencyGraphPerFramework); foreach (var frameworks in deduplicatedFrameworks) { - PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[frameworks.FirstOrDefault()], targetPackage); + PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[frameworks.FirstOrDefault()], targetPackage, logger); } } @@ -425,19 +414,19 @@ private static List> GetDeduplicatedFrameworks(DictionaryThe list of frameworks that share this dependency graph. /// The top-level package nodes of the dependency graph. /// The package we want the dependency paths for. - private static void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes, string targetPackage) + private static void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes, string targetPackage, ILoggerWithColor logger) { // print framework header foreach (var framework in frameworks) { - Console.WriteLine($"\t[{framework}]"); + logger.LogMinimal($"\t[{framework}]"); } - Console.WriteLine($"\t {ChildPrefixSymbol}"); + logger.LogMinimal($"\t {ChildPrefixSymbol}"); if (topLevelNodes == null || topLevelNodes.Count == 0) { - Console.WriteLine($"\t {LastChildNodeSymbol}{Strings.WhyCommand_Message_NoDependencyGraphsFoundForFramework}\n"); + logger.LogMinimal($"\t {LastChildNodeSymbol}{Strings.WhyCommand_Message_NoDependencyGraphsFoundForFramework}\n\n"); return; } @@ -470,14 +459,12 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li // print current node if (current.Node.Id == targetPackage) { - Console.Write($"{currentPrefix}"); - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine($"{current.Node.Id} (v{current.Node.Version})"); - Console.ResetColor(); + logger.LogMinimal($"{currentPrefix}", Console.ForegroundColor); + logger.LogMinimal($"{current.Node.Id} (v{current.Node.Version})\n", TargetPackageColor); } else { - Console.WriteLine($"{currentPrefix}{current.Node.Id} (v{current.Node.Version})"); + logger.LogMinimal($"{currentPrefix}{current.Node.Id} (v{current.Node.Version})"); } if (current.Node.Children?.Count > 0) @@ -491,7 +478,7 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li } } - Console.WriteLine(); + logger.LogMinimal(""); } /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/CommandOutputLogger.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/CommandOutputLogger.cs index c2bdc44cd5e..bc8b94b2700 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/CommandOutputLogger.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/CommandOutputLogger.cs @@ -128,7 +128,7 @@ public override Task LogAsync(ILogMessage message) return Task.CompletedTask; } - public void LogMinimal(string data, ConsoleColor color) + public virtual void LogMinimal(string data, ConsoleColor color) { var currentColor = Console.ForegroundColor; Console.ForegroundColor = color; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index 167d6311cc1..224375f7641 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using Microsoft.Build.Construction; @@ -659,7 +660,7 @@ internal static bool IsPackageReferenceProject(Project project) /// FrameworkPackages collection with top-level and transitive package/project /// references for each framework, or null on error internal List GetResolvedVersions( - Project project, IEnumerable userInputFrameworks, LockFile assetsFile, bool transitive) + Project project, IEnumerable userInputFrameworks, LockFile assetsFile, bool transitive, bool includeProjectReferences = false) { if (userInputFrameworks == null) { @@ -720,10 +721,14 @@ internal List GetResolvedVersions( // Find the tfminformation corresponding to the target to // get the top-level dependencies TargetFrameworkInformation tfmInformation; + IEnumerable projectReferences; try { tfmInformation = requestedTargetFrameworks.First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)); + projectReferences = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks + .First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)) + .ProjectReferences.Select(p => Path.GetRelativePath(projectPath, p.ProjectPath)); } catch (Exception) { @@ -741,11 +746,19 @@ internal List GetResolvedVersions( var matchingPackages = frameworkDependencies.Where(d => d.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase)).ToList(); + List matchingProjects = null; + if (library.Type == "project" && includeProjectReferences) + { + string projectLibraryPath = Path.GetFullPath(GetProjectLibraryPath(library, assetsFile.Libraries)); + matchingProjects ??= projectReferences.Where(p => + Path.GetFullPath(p).Equals(projectLibraryPath, StringComparison.OrdinalIgnoreCase)).ToList(); + } + var resolvedVersion = library.Version.ToString(); //In case we found a matching package in requestedVersions, the package will be //top level. - if (matchingPackages.Any()) + if (matchingPackages.Count > 0) { var topLevelPackage = matchingPackages.Single(); InstalledPackageReference installedPackage = default; @@ -793,11 +806,26 @@ internal List GetResolvedVersions( installedPackage.AutoReference = topLevelPackage.AutoReferenced; - if (library.Type != "project") + if (library.Type != "project" || includeProjectReferences) { topLevelPackages.Add(installedPackage); } } + else if (matchingProjects?.Count > 0) + { + var topLevelProject = matchingProjects.Single(); + var projectReference = new InstalledPackageReference(library.Name); + /* + { + OriginalRequestedVersion = resolvedVersion + }; + */ + + if (includeProjectReferences) + { + topLevelPackages.Add(projectReference); + } + } // If no matching packages were found, then the package is transitive, // and include-transitive must be used to add the package else if (transitive) // be sure to exclude "project" references here as these are irrelevant @@ -809,7 +837,7 @@ internal List GetResolvedVersions( .Build() }; - if (library.Type != "project") + if (library.Type != "project" || includeProjectReferences) { transitivePackages.Add(installedPackage); } @@ -827,6 +855,18 @@ internal List GetResolvedVersions( return resultPackages; } + /// + /// Gets the relative path of the given project library. + /// + /// The target project library that we want the path for. + /// All libraries in the assets file. + /// Relative path of the matching project library. + private string GetProjectLibraryPath(LockFileTargetLibrary library, IList libraries) + { + return libraries.Where(l => l.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase) + && l.Version.Equals(library.Version)) + .First().Path; + } /// /// Returns all package references after evaluating the condition on the item groups. diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/TestCommandOutputLogger.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/TestCommandOutputLogger.cs index aa7aebcecac..791c5bfb405 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/TestCommandOutputLogger.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/TestCommandOutputLogger.cs @@ -54,6 +54,11 @@ protected override void LogInternal(LogLevel logLevel, string message) } } + public override void LogMinimal(string data, ConsoleColor color) + { + LogInternal(LogLevel.Minimal, data); + } + public ConcurrentQueue Messages { get diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index cd6b9d256cc..f164e16cb38 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -57,8 +57,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( Assert.Equal(System.Threading.Tasks.Task.CompletedTask, result); Assert.Equal(string.Empty, logger.ShowErrors()); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.PackageName}'", output); - Assert.DoesNotContain($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageY.PackageName}'", output); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); } } @@ -115,5 +114,25 @@ public void WhyCommand_InvalidProject_Fails() Assert.DoesNotContain($"Project '{ProjectName}' does not have any dependency graph(s) for 'PackageX'", output); } } + + /* + [Fact] + public void NewTest() + { + var assetsFilePath = Path.Combine(pathContext.SolutionRoot, "obj", LockFileFormat.AssetsFileName); + var format = new LockFileFormat(); + LockFile assetsFile = format.Read(assetsFilePath); + + var responses = new Dictionary + { + { testFeedUrl, ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.index.json", GetType()) }, + { query + "?q=nuget&skip=0&take=26&prerelease=true&semVerLevel=2.0.0", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.nugetSearchPage1.json", GetType()) }, + { query + "?q=nuget&skip=25&take=26&prerelease=true&semVerLevel=2.0.0", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.nugetSearchPage2.json", GetType()) }, + { query + "?q=&skip=0&take=26&prerelease=true&semVerLevel=2.0.0", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.blankSearchPage.json", GetType()) }, + { "https://api.nuget.org/v3/registration3-gz-semver2/nuget.core/index.json", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.nugetCoreIndex.json", GetType()) }, + { "https://api.nuget.org/v3/registration3-gz-semver2/microsoft.extensions.logging.abstractions/index.json", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.loggingAbstractions.json", GetType()) } + }; + } + */ } } From fa67ec9c80a3eba7d5c4c9f482934d7764d8ccaf Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 9 May 2024 16:53:22 -0700 Subject: [PATCH 27/53] still wipping.... --- .../Commands/WhyCommand/WhyCommandRunner.cs | 5 +++ .../Utility/MSBuildAPIUtility.cs | 32 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 245bafa5b1a..81d81e85c17 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -147,6 +147,11 @@ private static void FindAllDependencyGraphs( doesProjectHaveDependencyOnPackage = true; dependencyGraphPerFramework.Add(frameworkPackage.Framework, GetDependencyGraphPerFramework(frameworkPackage.TopLevelPackages, packageLibraries, targetPackage)); + + if (dependencyGraphPerFramework[frameworkPackage.Framework] == null) + { + Console.WriteLine("WTF?"); + } } else { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index 224375f7641..fd1204a44cd 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -728,7 +728,14 @@ internal List GetResolvedVersions( tfmInformation = requestedTargetFrameworks.First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)); projectReferences = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks .First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)) - .ProjectReferences.Select(p => Path.GetRelativePath(projectPath, p.ProjectPath)); + .ProjectReferences.Select(p => p.ProjectPath); // need to filter by the alias too + + /* + projectReferences = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks + .First(tfm => tfm.FrameworkName.Equals(target.TargetFramework) + && tfm.TargetAlias.Equals(??)) // what goes here? + .ProjectReferences.Select(p => p.ProjectPath); + */ } catch (Exception) { @@ -749,9 +756,9 @@ internal List GetResolvedVersions( List matchingProjects = null; if (library.Type == "project" && includeProjectReferences) { - string projectLibraryPath = Path.GetFullPath(GetProjectLibraryPath(library, assetsFile.Libraries)); + string projectLibraryPath = GetProjectLibraryPath(library, assetsFile.Libraries, project.DirectoryPath); matchingProjects ??= projectReferences.Where(p => - Path.GetFullPath(p).Equals(projectLibraryPath, StringComparison.OrdinalIgnoreCase)).ToList(); + p.Equals(projectLibraryPath, StringComparison.OrdinalIgnoreCase)).ToList(); } var resolvedVersion = library.Version.ToString(); @@ -814,12 +821,10 @@ internal List GetResolvedVersions( else if (matchingProjects?.Count > 0) { var topLevelProject = matchingProjects.Single(); - var projectReference = new InstalledPackageReference(library.Name); - /* + var projectReference = new InstalledPackageReference(library.Name) { OriginalRequestedVersion = resolvedVersion }; - */ if (includeProjectReferences) { @@ -856,16 +861,19 @@ internal List GetResolvedVersions( } /// - /// Gets the relative path of the given project library. + /// Gets the absolute path of the given project library. /// /// The target project library that we want the path for. /// All libraries in the assets file. - /// Relative path of the matching project library. - private string GetProjectLibraryPath(LockFileTargetLibrary library, IList libraries) + /// The absolute path to the project file. + /// absolute path of the matching project library. + private string GetProjectLibraryPath(LockFileTargetLibrary library, IList libraries, string projectDirectoryPath) { - return libraries.Where(l => l.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase) - && l.Version.Equals(library.Version)) - .First().Path; + string relativePath = libraries.Where(l => l.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase) + && l.Version.Equals(library.Version)) + .First().Path; + + return Path.GetFullPath(relativePath, projectDirectoryPath); } /// From 961b7dff13e7487011da3eca7f8b15874f332d95 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 10 May 2024 12:52:36 -0700 Subject: [PATCH 28/53] added tests, TODOs for targetAlias matching in GetResolvedVersions; remove the TODOs from here --- .../Commands/WhyCommand/WhyCommandArgs.cs | 56 ------- .../Commands/WhyCommand/WhyCommandRunner.cs | 67 +++++++- .../Utility/MSBuildAPIUtility.cs | 19 ++- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 144 ++++++++++++++++-- 4 files changed, 212 insertions(+), 74 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs index 61735e54193..31ed44714d2 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs @@ -3,10 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using NuGet.Frameworks; namespace NuGet.CommandLine.XPlat { @@ -34,58 +30,6 @@ public WhyCommandArgs( Package = package ?? throw new ArgumentNullException(nameof(package)); Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - ValidatePathArgument(Path); - ValidatePackageArgument(Package); - ValidateFrameworksOption(Frameworks); - } - - private static void ValidatePathArgument(string path) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - "PROJECT|SOLUTION")); - } - - if (!File.Exists(path) - || (!path.EndsWith("proj", StringComparison.OrdinalIgnoreCase) - && !path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_PathIsMissingOrInvalid, - path)); - } - } - - private static void ValidatePackageArgument(string package) - { - if (string.IsNullOrEmpty(package)) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - "PACKAGE")); - } - } - - private static void ValidateFrameworksOption(List frameworks) - { - var parsedFrameworks = frameworks.Select(f => - NuGetFramework.Parse( - f.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .ToArray()[0])); - - if (parsedFrameworks.Any(f => f.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_InvalidFramework)); - } } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 81d81e85c17..9410f275346 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -6,8 +6,8 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Threading.Tasks; using Microsoft.Build.Evaluation; +using NuGet.Frameworks; using NuGet.ProjectModel; using NuGet.Shared; @@ -31,8 +31,20 @@ internal static class WhyCommandRunner /// Executes the 'why' command. /// /// CLI arguments for the 'why' command. - public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) + public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) { + try + { + ValidatePathArgument(whyCommandArgs.Path); + ValidatePackageArgument(whyCommandArgs.Package); + ValidateFrameworksOption(whyCommandArgs.Frameworks); + } + catch (ArgumentException ex) + { + whyCommandArgs.Logger.LogError(ex.Message); + return ExitCodes.InvalidArguments; + } + var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); string targetPackage = whyCommandArgs.Package; @@ -54,7 +66,55 @@ public static Task ExecuteCommand(WhyCommandArgs whyCommandArgs) ProjectCollection.GlobalProjectCollection.UnloadProject(project); } - return Task.CompletedTask; + return ExitCodes.Success; + } + + private static void ValidatePathArgument(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + "PROJECT|SOLUTION")); + } + + if (!File.Exists(path) + || (!path.EndsWith("proj", StringComparison.OrdinalIgnoreCase) + && !path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_PathIsMissingOrInvalid, + path)); + } + } + + private static void ValidatePackageArgument(string package) + { + if (string.IsNullOrEmpty(package)) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + "PACKAGE")); + } + } + + private static void ValidateFrameworksOption(List frameworks) + { + var parsedFrameworks = frameworks.Select(f => + NuGetFramework.Parse( + f.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .ToArray()[0])); + + if (parsedFrameworks.Any(f => f.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_InvalidFramework)); + } } /// @@ -148,6 +208,7 @@ private static void FindAllDependencyGraphs( dependencyGraphPerFramework.Add(frameworkPackage.Framework, GetDependencyGraphPerFramework(frameworkPackage.TopLevelPackages, packageLibraries, targetPackage)); + // Sanity check if (dependencyGraphPerFramework[frameworkPackage.Framework] == null) { Console.WriteLine("WTF?"); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index fd1204a44cd..cd6d096af78 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -682,15 +682,24 @@ internal List GetResolvedVersions( var requestedTargetFrameworks = assetsFile.PackageSpec.TargetFrameworks; var requestedTargets = assetsFile.Targets; + var requestedRestoreMetadataTFMs = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks; //TODO + // If the user has entered frameworks, we want to filter // the targets and frameworks from the assets file if (userInputFrameworks.Any()) { + // TODO: If we're supposed to use aliases instead of actual frameworks here, + // do we 1) ignore the first line where we're parsing them into NuGetFrameworks, + // and 2) match the user input string against tfm.TargetAlias and not tfm.FrameworkName instead? //Target frameworks filtering var parsedUserFrameworks = userInputFrameworks.Select(f => NuGetFramework.Parse(f.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray()[0])); requestedTargetFrameworks = requestedTargetFrameworks.Where(tfm => parsedUserFrameworks.Contains(tfm.FrameworkName)).ToList(); + // TODO: i.e. + requestedTargetFrameworks = requestedTargetFrameworks.Where(tfm => userInputFrameworks.Contains(tfm.TargetAlias)).ToList(); + requestedRestoreMetadataTFMs = requestedRestoreMetadataTFMs.Where(tfm => userInputFrameworks.Contains(tfm.TargetAlias)).ToList(); + //Assets file targets filtering by framework and RID var filteredTargets = new List(); foreach (var frameworkAndRID in userInputFrameworks) @@ -728,7 +737,15 @@ internal List GetResolvedVersions( tfmInformation = requestedTargetFrameworks.First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)); projectReferences = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks .First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)) - .ProjectReferences.Select(p => p.ProjectPath); // need to filter by the alias too + .ProjectReferences.Select(p => p.ProjectPath); + // TODO: need to filter by alias here? 1) why, (only when we have user input frameworks in the CLI options?) + // and 2) how exactly? + // filter by alias + framework? alias instead of framework? + // where would the the alias value come from? One is from the package spec, what about the other one? User input CLI option? + // Now, we're already filtering it by alias above, if there are any user input frameworks. Is that what was needed? + + // TODO: This refactoring might introduce changes/bugs in other places, like the list command. + // Deal with this later, and just match based on frameworks normally right now? /* projectReferences = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index f164e16cb38..ed80e98ef70 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -1,6 +1,8 @@ // 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 Moq; using NuGet.CommandLine.XPlat; using NuGet.Packaging; using NuGet.Test.Utility; @@ -15,7 +17,7 @@ public class XPlatWhyTests private static MSBuildAPIUtility MsBuild => new MSBuildAPIUtility(new TestCommandOutputLogger()); [Fact] - public async void WhyCommand_BasicFunctionality_Succeeds() + public async void WhyCommand_TransitiveDependency_PathExists() { // Arrange using (var pathContext = new SimpleTestPathContext()) @@ -54,9 +56,102 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( // Assert var output = logger.ShowMessages(); - Assert.Equal(System.Threading.Tasks.Task.CompletedTask, result); - Assert.Equal(string.Empty, logger.ShowErrors()); + Assert.Equal(ExitCodes.Success, result); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); + } + } + + [Fact] + public async void WhyCommand_TransitiveDependency_OutputFormatIsCorrect() + { + // Arrange + using (var pathContext = new SimpleTestPathContext()) + { + var logger = new Mock(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + // Generate Package + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); + var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); + var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); + + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + packageY.Id, + [projectFramework], + logger.Object); + + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + // Assert + Assert.Equal(ExitCodes.Success, result); + + logger.Verify(x => x.LogMinimal("Project 'Test.Project.DotnetNugetWhy' has the following dependency graph(s) for 'PackageY':"), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(""), Times.Exactly(2)); + logger.Verify(x => x.LogMinimal(" [net472]"), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" │ "), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" └─ PackageX (v1.0.0)"), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" └─ ", ConsoleColor.Gray), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)", ConsoleColor.Cyan), Times.Exactly(1)); + } + } + + [Fact] + public async void WhyCommand_BasicFunctionality_Succeeds() + { + // Arrange + using (var pathContext = new SimpleTestPathContext()) + { + var logger = new TestCommandOutputLogger(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + // Generate Package + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); + var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); + var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); + + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + packageY.Id, + [projectFramework], + logger); + + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var output = logger.ShowMessages(); + + Assert.Equal(ExitCodes.Success, result); Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); } } @@ -78,13 +173,37 @@ public void WhyCommand_EmptyProjectArgument_Fails() var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var output = logger.ShowMessages(); + var errorOutput = logger.ShowErrors(); + + Assert.Equal(ExitCodes.InvalidArguments, result); + Assert.Contains($"Unable to run 'dotnet nuget why'. The 'PROJECT|SOLUTION' argument cannot be empty.", errorOutput); + } + } + + [Fact] + public void WhyCommand_EmptyPackageArgument_Fails() + { + // Arrange + using (var pathContext = new SimpleTestPathContext()) + { + var logger = new TestCommandOutputLogger(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - Assert.Equal(System.Threading.Tasks.Task.CompletedTask, result); - Assert.Equal(string.Empty, logger.ShowErrors()); + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + "", + [], + logger); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for 'PackageX'", output); - Assert.DoesNotContain($"Project '{ProjectName}' does not have any dependency graph(s) for 'PackageX'", output); + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var errorOutput = logger.ShowErrors(); + + Assert.Equal(ExitCodes.InvalidArguments, result); + Assert.Contains($"Unable to run 'dotnet nuget why'. The 'PACKAGE' argument cannot be empty.", errorOutput); } } @@ -105,13 +224,10 @@ public void WhyCommand_InvalidProject_Fails() var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); // Assert - var output = logger.ShowMessages(); - - Assert.Equal(System.Threading.Tasks.Task.CompletedTask, result); - Assert.Equal(string.Empty, logger.ShowErrors()); + var errorOutput = logger.ShowErrors(); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for 'PackageX'", output); - Assert.DoesNotContain($"Project '{ProjectName}' does not have any dependency graph(s) for 'PackageX'", output); + Assert.Equal(ExitCodes.InvalidArguments, result); + Assert.Contains($"Unable to run 'dotnet nuget why'. Missing or invalid project/solution file 'FakeProjectPath.csproj'.", errorOutput); } } From cf5771f3f4b37e5ec05b7439fed7b29faab050d7 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 10 May 2024 13:08:12 -0700 Subject: [PATCH 29/53] added new test files, will fill them out later --- .../Dotnet.Integration.Test/DotnetWhyTests.cs | 80 ++++++++++++++ .../XPlatWhyUnitTests.cs | 104 ++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs create mode 100644 test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs new file mode 100644 index 00000000000..ed88fc070bd --- /dev/null +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -0,0 +1,80 @@ +// 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 System; +using NuGet.CommandLine.Xplat.Tests; +using NuGet.Test.Utility; +using Xunit; +using System.IO; +using System.Reflection; + +namespace Dotnet.Integration.Test +{ + [Collection(DotnetIntegrationCollection.Name)] + public class DotnetWhyTests : IClassFixture + { + private readonly DotnetIntegrationTestFixture _testFixture; + private readonly PackageSearchRunnerFixture _packageSearchRunnerFixture; + + public DotnetWhyTests(DotnetIntegrationTestFixture testFixture, PackageSearchRunnerFixture packageSearchRunnerFixture) + { + _testFixture = testFixture; + _packageSearchRunnerFixture = packageSearchRunnerFixture; + } + + internal string NormalizeNewlines(string input) + { + return input.Replace("\r\n", "\n").Replace("\r", "\n"); + } + + [Fact] + public void DotnetPackageSearch_Succeed() + { + using (var pathContext = new SimpleTestPathContext()) + { + // Arrange + var args = new string[] { "package", "search", "json", "--take", "10", "--prerelease", "--source", $"{_packageSearchRunnerFixture.ServerWithMultipleEndpoints.Uri}v3/index.json", "--format", "json" }; + + // Act + var result = _testFixture.RunDotnetExpectSuccess(pathContext.PackageSource, string.Join(" ", args)); + + // Assert + Assert.Equal(0, result.ExitCode); + Assert.Contains("\"id\": \"Fake.Newtonsoft.Json\",", result.AllOutput); + Assert.Contains("\"owners\": \"James Newton-King\"", result.AllOutput); + Assert.Contains("\"totalDownloads\": 531607259,", result.AllOutput); + Assert.Contains("\"latestVersion\": \"12.0.3\"", result.AllOutput); + } + } + + [Fact] + public void DotnetPackageSearch_WithInvalidSource_FailWithNoHelpOutput() + { + using (var pathContext = new SimpleTestPathContext()) + { + // Arrange + string source = "invalid-source"; + var args = new string[] { "package", "search", "json", "--source", source, "--format", "json" }; + Dictionary finalEnvironmentVariables = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["MSBuildSDKsPath"] = _testFixture.MsBuildSdksPath, + ["UseSharedCompilation"] = bool.FalseString, + ["DOTNET_MULTILEVEL_LOOKUP"] = "0", + ["DOTNET_ROOT"] = TestDotnetCLiUtility.CopyAndPatchLatestDotnetCli(Path.GetFullPath(Assembly.GetExecutingAssembly().Location)), + ["MSBUILDDISABLENODEREUSE"] = bool.TrueString, + ["NUGET_SHOW_STACK"] = bool.TrueString + }; + + string error = "is invalid. Provide a valid source."; + string help = "dotnet package search [] [options]"; + // Act + var result = CommandRunner.Run(_testFixture.TestDotnetCli, pathContext.PackageSource, string.Join(" ", args), environmentVariables: finalEnvironmentVariables); + + // Assert + Assert.Contains(error, result.AllOutput); + Assert.DoesNotContain(help, result.AllOutput); + } + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs new file mode 100644 index 00000000000..3329a337155 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs @@ -0,0 +1,104 @@ +// 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 NuGet.CommandLine.XPlat; +using Xunit; + +namespace NuGet.CommandLine.Xplat.Tests +{ + public class XPlatWhyUnitTests + { + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void ConfigPathsCommand_WithNullArguments_ThrowsArgumentNullException(bool argsIsNull, bool loggerIsNull) + { + // Arrange + ConfigPathsArgs args = null; + CommandOutputLogger getLogger = null; + + if (!argsIsNull) + { + args = new ConfigPathsArgs(); + } + + if (!loggerIsNull) + { + getLogger = new CommandOutputLogger(Common.LogLevel.Debug); + } + + // Act & Assert + Assert.Throws(() => ConfigPathsRunner.Run(args, loggerIsNull ? null : () => getLogger)); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void ConfigGetCommand_WithNullArguments_ThrowsArgumentNullException(bool argsIsNull, bool loggerIsNull) + { + // Arrange + ConfigGetArgs args = null; + CommandOutputLogger getLogger = null; + + if (!argsIsNull) + { + args = new ConfigGetArgs(); + } + + if (!loggerIsNull) + { + getLogger = new CommandOutputLogger(Common.LogLevel.Debug); + } + + // Act & Assert + Assert.Throws(() => ConfigGetRunner.Run(args, loggerIsNull ? null : () => getLogger)); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void ConfigSetCommand_WithNullArguments_ThrowsArgumentNullException(bool argsIsNull, bool loggerIsNull) + { + // Arrange + ConfigSetArgs args = null; + CommandOutputLogger getLogger = null; + + if (!argsIsNull) + { + args = new ConfigSetArgs(); + } + + if (!loggerIsNull) + { + getLogger = new CommandOutputLogger(Common.LogLevel.Debug); + } + + // Act & Assert + Assert.Throws(() => ConfigSetRunner.Run(args, loggerIsNull ? null : () => getLogger)); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void ConfigUnsetCommand_WithNullArguments_ThrowsArgumentNullException(bool argsIsNull, bool loggerIsNull) + { + // Arrange + ConfigUnsetArgs args = null; + CommandOutputLogger getLogger = null; + + if (!argsIsNull) + { + args = new ConfigUnsetArgs(); + } + + if (!loggerIsNull) + { + getLogger = new CommandOutputLogger(Common.LogLevel.Debug); + } + + // Act & Assert + Assert.Throws(() => ConfigUnsetRunner.Run(args, loggerIsNull ? null : () => getLogger)); + } + } +} From efaee797a2e6e45e8ff9c7b90fbbe10d5d13dcfe Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 10 May 2024 13:19:42 -0700 Subject: [PATCH 30/53] fixed failing test --- test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index ed80e98ef70..cb638ff6076 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -107,7 +107,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( logger.Verify(x => x.LogMinimal(" │ "), Times.Exactly(1)); logger.Verify(x => x.LogMinimal(" └─ PackageX (v1.0.0)"), Times.Exactly(1)); logger.Verify(x => x.LogMinimal(" └─ ", ConsoleColor.Gray), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)", ConsoleColor.Cyan), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)\n", ConsoleColor.Cyan), Times.Exactly(1)); } } From 0bba3a1585e4fb1bce912b3e0b9d577fcb00992d Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Fri, 10 May 2024 14:48:06 -0700 Subject: [PATCH 31/53] addressin smore feedback --- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 306 +++++++++--------- 1 file changed, 148 insertions(+), 158 deletions(-) diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index cb638ff6076..b535043c352 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -19,216 +19,206 @@ public class XPlatWhyTests [Fact] public async void WhyCommand_TransitiveDependency_PathExists() { - // Arrange - using (var pathContext = new SimpleTestPathContext()) - { - var logger = new TestCommandOutputLogger(); - var projectFramework = "net472"; - var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + // Arrange + var logger = new TestCommandOutputLogger(); - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - packageX.Dependencies.Add(packageY); + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); - project.AddPackageToFramework(projectFramework, packageX); + packageX.Dependencies.Add(packageY); - // Generate Package - await SimpleTestPackageUtility.CreateFolderFeedV3Async( - pathContext.PackageSource, - PackageSaveMode.Defaultv3, - packageX, - packageY); + project.AddPackageToFramework(projectFramework, packageX); - var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); - var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); - var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); - var whyCommandArgs = new WhyCommandArgs( - project.ProjectPath, - packageY.Id, - [projectFramework], - logger); + var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); + var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); + var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); - // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + packageY.Id, + [projectFramework], + logger); - // Assert - var output = logger.ShowMessages(); + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); - Assert.Equal(ExitCodes.Success, result); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); - } + // Assert + var output = logger.ShowMessages(); + + Assert.Equal(ExitCodes.Success, result); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); } [Fact] public async void WhyCommand_TransitiveDependency_OutputFormatIsCorrect() { // Arrange - using (var pathContext = new SimpleTestPathContext()) - { - var logger = new Mock(); - var projectFramework = "net472"; - var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); - - packageX.Dependencies.Add(packageY); - - project.AddPackageToFramework(projectFramework, packageX); - - // Generate Package - await SimpleTestPackageUtility.CreateFolderFeedV3Async( - pathContext.PackageSource, - PackageSaveMode.Defaultv3, - packageX, - packageY); - - var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); - var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); - var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); - - var whyCommandArgs = new WhyCommandArgs( - project.ProjectPath, - packageY.Id, - [projectFramework], - logger.Object); - - // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); - - // Assert - Assert.Equal(ExitCodes.Success, result); - - logger.Verify(x => x.LogMinimal("Project 'Test.Project.DotnetNugetWhy' has the following dependency graph(s) for 'PackageY':"), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(""), Times.Exactly(2)); - logger.Verify(x => x.LogMinimal(" [net472]"), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" │ "), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" └─ PackageX (v1.0.0)"), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" └─ ", ConsoleColor.Gray), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)\n", ConsoleColor.Cyan), Times.Exactly(1)); - } + var logger = new Mock(); + + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); + var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); + var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); + + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + packageY.Id, + [projectFramework], + logger.Object); + + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + Assert.Equal(ExitCodes.Success, result); + + logger.Verify(x => x.LogMinimal("Project 'Test.Project.DotnetNugetWhy' has the following dependency graph(s) for 'PackageY':"), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(""), Times.Exactly(2)); + logger.Verify(x => x.LogMinimal(" [net472]"), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" │ "), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" └─ PackageX (v1.0.0)"), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" └─ ", ConsoleColor.Gray), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)\n", ConsoleColor.Cyan), Times.Exactly(1)); } [Fact] public async void WhyCommand_BasicFunctionality_Succeeds() { // Arrange - using (var pathContext = new SimpleTestPathContext()) - { - var logger = new TestCommandOutputLogger(); - var projectFramework = "net472"; - var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + var logger = new TestCommandOutputLogger(); + + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); - packageX.Dependencies.Add(packageY); + packageX.Dependencies.Add(packageY); - project.AddPackageToFramework(projectFramework, packageX); + project.AddPackageToFramework(projectFramework, packageX); - // Generate Package - await SimpleTestPackageUtility.CreateFolderFeedV3Async( - pathContext.PackageSource, - PackageSaveMode.Defaultv3, - packageX, - packageY); + // Generate Package + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); - var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); - var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); - var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); + var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); + var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); + var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); - var whyCommandArgs = new WhyCommandArgs( - project.ProjectPath, - packageY.Id, - [projectFramework], - logger); + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + packageY.Id, + [projectFramework], + logger); - // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); - // Assert - var output = logger.ShowMessages(); + // Assert + var output = logger.ShowMessages(); - Assert.Equal(ExitCodes.Success, result); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); - } + Assert.Equal(ExitCodes.Success, result); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); } [Fact] public void WhyCommand_EmptyProjectArgument_Fails() { // Arrange - using (var pathContext = new SimpleTestPathContext()) - { - var logger = new TestCommandOutputLogger(); - var whyCommandArgs = new WhyCommandArgs( - "", - "PackageX", - [], - logger); - - // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); - - // Assert - var errorOutput = logger.ShowErrors(); - - Assert.Equal(ExitCodes.InvalidArguments, result); - Assert.Contains($"Unable to run 'dotnet nuget why'. The 'PROJECT|SOLUTION' argument cannot be empty.", errorOutput); - } + var logger = new TestCommandOutputLogger(); + + var whyCommandArgs = new WhyCommandArgs( + "", + "PackageX", + [], + logger); + + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var errorOutput = logger.ShowErrors(); + + Assert.Equal(ExitCodes.InvalidArguments, result); + Assert.Contains($"Unable to run 'dotnet nuget why'. The 'PROJECT|SOLUTION' argument cannot be empty.", errorOutput); } [Fact] public void WhyCommand_EmptyPackageArgument_Fails() { // Arrange - using (var pathContext = new SimpleTestPathContext()) - { - var logger = new TestCommandOutputLogger(); - var projectFramework = "net472"; - var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + var logger = new TestCommandOutputLogger(); + + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - var whyCommandArgs = new WhyCommandArgs( - project.ProjectPath, - "", - [], - logger); + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + "", + [], + logger); - // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); - // Assert - var errorOutput = logger.ShowErrors(); + // Assert + var errorOutput = logger.ShowErrors(); - Assert.Equal(ExitCodes.InvalidArguments, result); - Assert.Contains($"Unable to run 'dotnet nuget why'. The 'PACKAGE' argument cannot be empty.", errorOutput); - } + Assert.Equal(ExitCodes.InvalidArguments, result); + Assert.Contains($"Unable to run 'dotnet nuget why'. The 'PACKAGE' argument cannot be empty.", errorOutput); } [Fact] public void WhyCommand_InvalidProject_Fails() { // Arrange - using (var pathContext = new SimpleTestPathContext()) - { - var logger = new TestCommandOutputLogger(); - var whyCommandArgs = new WhyCommandArgs( - "FakeProjectPath.csproj", - "PackageX", - [], - logger); - - // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); - - // Assert - var errorOutput = logger.ShowErrors(); - - Assert.Equal(ExitCodes.InvalidArguments, result); - Assert.Contains($"Unable to run 'dotnet nuget why'. Missing or invalid project/solution file 'FakeProjectPath.csproj'.", errorOutput); - } + var logger = new TestCommandOutputLogger(); + + var whyCommandArgs = new WhyCommandArgs( + "FakeProjectPath.csproj", + "PackageX", + [], + logger); + + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var errorOutput = logger.ShowErrors(); + + Assert.Equal(ExitCodes.InvalidArguments, result); + Assert.Contains($"Unable to run 'dotnet nuget why'. Missing or invalid project/solution file 'FakeProjectPath.csproj'.", errorOutput); } /* From 8ee8eba7901deed9db83a74187981cca312e4fb4 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Mon, 13 May 2024 05:49:49 -0700 Subject: [PATCH 32/53] added func tests + dotnet integration tests --- .../Commands/WhyCommand/WhyCommand.cs | 2 +- .../Dotnet.Integration.Test/DotnetWhyTests.cs | 216 +++++++++++++----- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 53 ++++- 3 files changed, 207 insertions(+), 64 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs index 51a95ce6ff2..99b8f9160d6 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs @@ -22,7 +22,7 @@ internal static class WhyCommand Arity = ArgumentArity.ExactlyOne }; - private static CliOption> Frameworks = new CliOption>("-f|--framework") + private static CliOption> Frameworks = new CliOption>("--framework", "-f") { Description = Strings.WhyCommand_FrameworksOption_Description, Arity = ArgumentArity.OneOrMore diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index ed88fc070bd..ee2c8d60c9b 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -1,80 +1,192 @@ // 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 System; -using NuGet.CommandLine.Xplat.Tests; +using NuGet.CommandLine.XPlat; +using NuGet.Packaging; using NuGet.Test.Utility; +using NuGet.XPlat.FuncTest; using Xunit; -using System.IO; -using System.Reflection; namespace Dotnet.Integration.Test { [Collection(DotnetIntegrationCollection.Name)] - public class DotnetWhyTests : IClassFixture + public class DotnetWhyTests { + private static readonly string ProjectName = "Test.Project.DotnetNugetWhy"; + private readonly DotnetIntegrationTestFixture _testFixture; - private readonly PackageSearchRunnerFixture _packageSearchRunnerFixture; - public DotnetWhyTests(DotnetIntegrationTestFixture testFixture, PackageSearchRunnerFixture packageSearchRunnerFixture) + public DotnetWhyTests(DotnetIntegrationTestFixture testFixture) { _testFixture = testFixture; - _packageSearchRunnerFixture = packageSearchRunnerFixture; } - internal string NormalizeNewlines(string input) + [Fact] + public async void WhyCommand_SimpleTransitiveDependency_PathExists() { - return input.Replace("\r\n", "\n").Replace("\r", "\n"); + // Arrange + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; + CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs); + + string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id}"; + + // Act + CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs); + + // Assert + Assert.Equal(ExitCodes.Success, result.ExitCode); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); } [Fact] - public void DotnetPackageSearch_Succeed() + public async void WhyCommand_SimpleTransitiveDependency_WithFrameworksOption_PathExists() { - using (var pathContext = new SimpleTestPathContext()) - { - // Arrange - var args = new string[] { "package", "search", "json", "--take", "10", "--prerelease", "--source", $"{_packageSearchRunnerFixture.ServerWithMultipleEndpoints.Uri}v3/index.json", "--format", "json" }; - - // Act - var result = _testFixture.RunDotnetExpectSuccess(pathContext.PackageSource, string.Join(" ", args)); - - // Assert - Assert.Equal(0, result.ExitCode); - Assert.Contains("\"id\": \"Fake.Newtonsoft.Json\",", result.AllOutput); - Assert.Contains("\"owners\": \"James Newton-King\"", result.AllOutput); - Assert.Contains("\"totalDownloads\": 531607259,", result.AllOutput); - Assert.Contains("\"latestVersion\": \"12.0.3\"", result.AllOutput); - } + // Arrange + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; + CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs); + + string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id} --framework {projectFramework}"; + + // Act + CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs); + + // Assert + Assert.Equal(ExitCodes.Success, result.ExitCode); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); } [Fact] - public void DotnetPackageSearch_WithInvalidSource_FailWithNoHelpOutput() + public async void WhyCommand_SimpleTransitiveDependency_WithFrameworksOptionAlias_PathExists() { - using (var pathContext = new SimpleTestPathContext()) - { - // Arrange - string source = "invalid-source"; - var args = new string[] { "package", "search", "json", "--source", source, "--format", "json" }; - Dictionary finalEnvironmentVariables = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["MSBuildSDKsPath"] = _testFixture.MsBuildSdksPath, - ["UseSharedCompilation"] = bool.FalseString, - ["DOTNET_MULTILEVEL_LOOKUP"] = "0", - ["DOTNET_ROOT"] = TestDotnetCLiUtility.CopyAndPatchLatestDotnetCli(Path.GetFullPath(Assembly.GetExecutingAssembly().Location)), - ["MSBUILDDISABLENODEREUSE"] = bool.TrueString, - ["NUGET_SHOW_STACK"] = bool.TrueString - }; - - string error = "is invalid. Provide a valid source."; - string help = "dotnet package search [] [options]"; - // Act - var result = CommandRunner.Run(_testFixture.TestDotnetCli, pathContext.PackageSource, string.Join(" ", args), environmentVariables: finalEnvironmentVariables); - - // Assert - Assert.Contains(error, result.AllOutput); - Assert.DoesNotContain(help, result.AllOutput); - } + // Arrange + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; + CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs); + + string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id} -f {projectFramework}"; + + // Act + CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs); + + // Assert + Assert.Equal(ExitCodes.Success, result.ExitCode); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); + } + + [Fact] + public async void WhyCommand_SimpleTransitiveDependency_PathDoesNotExist() + { + // Arrange + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + project.AddPackageToFramework(projectFramework, packageX); + + var packageZ = XPlatTestUtils.CreatePackage("PackageZ", "1.0.0"); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageZ); + + string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; + CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs); + + string whyCommandArgs = $"nuget why {project.ProjectPath} {packageZ.Id}"; + + // Act + CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs); + + // Assert + Assert.Equal(ExitCodes.Success, result.ExitCode); + Assert.Contains($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageZ.Id}'", result.AllOutput); + } + + [Fact] + public void WhyCommand_EmptyProjectArgument_Fails() + { + // Arrange + var pathContext = new SimpleTestPathContext(); + + string whyCommandArgs = $"nuget why"; + + // Act + CommandRunnerResult result = _testFixture.RunDotnetExpectFailure(pathContext.SolutionRoot, whyCommandArgs); + + // Assert + Assert.Equal(ExitCodes.InvalidArguments, result.ExitCode); + Assert.Contains($"Required argument missing for command: 'why'.", result.Errors); + } + + [Fact] + public void WhyCommand_EmptyPackageArgument_Fails() + { + // Arrange + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + string whyCommandArgs = $"nuget why {project.ProjectPath}"; + + // Act + CommandRunnerResult result = _testFixture.RunDotnetExpectFailure(pathContext.SolutionRoot, whyCommandArgs); + + // Assert + Assert.Equal(ExitCodes.InvalidArguments, result.ExitCode); + Assert.Contains($"Required argument missing for command: 'why'.", result.Errors); } } } diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index b535043c352..f89c58a26ac 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -17,9 +17,9 @@ public class XPlatWhyTests private static MSBuildAPIUtility MsBuild => new MSBuildAPIUtility(new TestCommandOutputLogger()); [Fact] - public async void WhyCommand_TransitiveDependency_PathExists() + public async void WhyCommand_SimpleTransitiveDependency_PathExists() { - // Arrange + // Arrange var logger = new TestCommandOutputLogger(); var pathContext = new SimpleTestPathContext(); @@ -60,7 +60,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } [Fact] - public async void WhyCommand_TransitiveDependency_OutputFormatIsCorrect() + public async void WhyCommand_SimpleTransitiveDependency_OutputFormatIsCorrect() { // Arrange var logger = new Mock(); @@ -107,8 +107,9 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)\n", ConsoleColor.Cyan), Times.Exactly(1)); } + [Fact] - public async void WhyCommand_BasicFunctionality_Succeeds() + public async void WhyCommand_SimpleTransitiveDependency_PathDoesNotExist() { // Arrange var logger = new TestCommandOutputLogger(); @@ -118,23 +119,53 @@ public async void WhyCommand_BasicFunctionality_Succeeds() var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); - - packageX.Dependencies.Add(packageY); - project.AddPackageToFramework(projectFramework, packageX); - // Generate Package + var packageZ = XPlatTestUtils.CreatePackage("PackageZ", "1.0.0"); // not added to project + await SimpleTestPackageUtility.CreateFolderFeedV3Async( pathContext.PackageSource, PackageSaveMode.Defaultv3, packageX, - packageY); + packageZ); var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + packageZ.Id, + [projectFramework], + logger); + + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var output = logger.ShowMessages(); + + Assert.Equal(ExitCodes.Success, result); + Assert.Contains($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageZ.Id}'", output); + } + + [Fact] + public void WhyCommand_SimpleTransitiveDependency_DidNotRunRestore_Fails() + { + // Arrange + var logger = new TestCommandOutputLogger(); + + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + var whyCommandArgs = new WhyCommandArgs( project.ProjectPath, packageY.Id, @@ -148,7 +179,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var output = logger.ShowMessages(); Assert.Equal(ExitCodes.Success, result); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); + Assert.Contains($"No assets file was found for `{project.ProjectPath}`. Please run restore before running this command.", output); } [Fact] From 31a6bf3bb62d0938e9fd3590dd8a583a6048a5ff Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Mon, 13 May 2024 06:52:03 -0700 Subject: [PATCH 33/53] cleaning up for PR --- .../Commands/WhyCommand/WhyCommandRunner.cs | 3 +- .../Utility/MSBuildAPIUtility.cs | 31 +----- .../Dotnet.Integration.Test/DotnetWhyTests.cs | 34 +++--- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 29 +---- .../XPlatWhyUnitTests.cs | 104 ------------------ 5 files changed, 26 insertions(+), 175 deletions(-) delete mode 100644 test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 9410f275346..4e27c598b5a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -182,7 +182,6 @@ private static void FindAllDependencyGraphs( Project project, LockFile assetsFile) { - // TODO: The top-level packages here are only package references, and do not include project references. Need to fix this. // get all resolved package references for a project List frameworkPackages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true, includeProjectReferences: true); @@ -194,7 +193,7 @@ private static void FindAllDependencyGraphs( foreach (var frameworkPackage in frameworkPackages) { - LockFileTarget target = assetsFile.GetTarget(frameworkPackage.Framework, runtimeIdentifier: null); // runtime Identifier? + LockFileTarget target = assetsFile.GetTarget(frameworkPackage.Framework, runtimeIdentifier: null); if (target != default) { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index cd6d096af78..a1ed3df5fc6 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -657,6 +657,7 @@ internal static bool IsPackageReferenceProject(Project project) /// A list of frameworks /// Assets file for all targets and libraries /// Include transitive packages/projects in the result + /// Include project references in the result /// FrameworkPackages collection with top-level and transitive package/project /// references for each framework, or null on error internal List GetResolvedVersions( @@ -682,24 +683,15 @@ internal List GetResolvedVersions( var requestedTargetFrameworks = assetsFile.PackageSpec.TargetFrameworks; var requestedTargets = assetsFile.Targets; - var requestedRestoreMetadataTFMs = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks; //TODO - // If the user has entered frameworks, we want to filter // the targets and frameworks from the assets file if (userInputFrameworks.Any()) { - // TODO: If we're supposed to use aliases instead of actual frameworks here, - // do we 1) ignore the first line where we're parsing them into NuGetFrameworks, - // and 2) match the user input string against tfm.TargetAlias and not tfm.FrameworkName instead? //Target frameworks filtering var parsedUserFrameworks = userInputFrameworks.Select(f => NuGetFramework.Parse(f.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray()[0])); requestedTargetFrameworks = requestedTargetFrameworks.Where(tfm => parsedUserFrameworks.Contains(tfm.FrameworkName)).ToList(); - // TODO: i.e. - requestedTargetFrameworks = requestedTargetFrameworks.Where(tfm => userInputFrameworks.Contains(tfm.TargetAlias)).ToList(); - requestedRestoreMetadataTFMs = requestedRestoreMetadataTFMs.Where(tfm => userInputFrameworks.Contains(tfm.TargetAlias)).ToList(); - //Assets file targets filtering by framework and RID var filteredTargets = new List(); foreach (var frameworkAndRID in userInputFrameworks) @@ -738,21 +730,6 @@ internal List GetResolvedVersions( projectReferences = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks .First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)) .ProjectReferences.Select(p => p.ProjectPath); - // TODO: need to filter by alias here? 1) why, (only when we have user input frameworks in the CLI options?) - // and 2) how exactly? - // filter by alias + framework? alias instead of framework? - // where would the the alias value come from? One is from the package spec, what about the other one? User input CLI option? - // Now, we're already filtering it by alias above, if there are any user input frameworks. Is that what was needed? - - // TODO: This refactoring might introduce changes/bugs in other places, like the list command. - // Deal with this later, and just match based on frameworks normally right now? - - /* - projectReferences = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks - .First(tfm => tfm.FrameworkName.Equals(target.TargetFramework) - && tfm.TargetAlias.Equals(??)) // what goes here? - .ProjectReferences.Select(p => p.ProjectPath); - */ } catch (Exception) { @@ -882,13 +859,13 @@ internal List GetResolvedVersions( /// /// The target project library that we want the path for. /// All libraries in the assets file. - /// The absolute path to the project file. - /// absolute path of the matching project library. + /// The absolute path to the project base directory. + /// The absolute path of the matching project library. private string GetProjectLibraryPath(LockFileTargetLibrary library, IList libraries, string projectDirectoryPath) { string relativePath = libraries.Where(l => l.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase) && l.Version.Equals(library.Version)) - .First().Path; + .FirstOrDefault().Path; return Path.GetFullPath(relativePath, projectDirectoryPath); } diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index ee2c8d60c9b..df0f2d0136f 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -22,7 +22,7 @@ public DotnetWhyTests(DotnetIntegrationTestFixture testFixture) } [Fact] - public async void WhyCommand_SimpleTransitiveDependency_PathExists() + public async void WhyCommand_ProjectHasTransitiveDependency_DependencyPathExists() { // Arrange var pathContext = new SimpleTestPathContext(); @@ -56,7 +56,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } [Fact] - public async void WhyCommand_SimpleTransitiveDependency_WithFrameworksOption_PathExists() + public async void WhyCommand_ProjectHasNoDependencyOnTargetPackage_PathDoesNotExist() { // Arrange var pathContext = new SimpleTestPathContext(); @@ -64,33 +64,31 @@ public async void WhyCommand_SimpleTransitiveDependency_WithFrameworksOption_Pat var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); - - packageX.Dependencies.Add(packageY); - project.AddPackageToFramework(projectFramework, packageX); + var packageZ = XPlatTestUtils.CreatePackage("PackageZ", "1.0.0"); + await SimpleTestPackageUtility.CreateFolderFeedV3Async( pathContext.PackageSource, PackageSaveMode.Defaultv3, packageX, - packageY); + packageZ); string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs); - string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id} --framework {projectFramework}"; + string whyCommandArgs = $"nuget why {project.ProjectPath} {packageZ.Id}"; // Act CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs); // Assert Assert.Equal(ExitCodes.Success, result.ExitCode); - Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); + Assert.Contains($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageZ.Id}'", result.AllOutput); } [Fact] - public async void WhyCommand_SimpleTransitiveDependency_WithFrameworksOptionAlias_PathExists() + public async void WhyCommand_WithFrameworksOption_OptionParsedSuccessfully() { // Arrange var pathContext = new SimpleTestPathContext(); @@ -113,7 +111,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs); - string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id} -f {projectFramework}"; + string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id} --framework {projectFramework}"; // Act CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs); @@ -124,7 +122,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } [Fact] - public async void WhyCommand_SimpleTransitiveDependency_PathDoesNotExist() + public async void WhyCommand_WithFrameworksOptionAlias_OptionParsedSuccessfully() { // Arrange var pathContext = new SimpleTestPathContext(); @@ -132,27 +130,29 @@ public async void WhyCommand_SimpleTransitiveDependency_PathDoesNotExist() var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - project.AddPackageToFramework(projectFramework, packageX); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); - var packageZ = XPlatTestUtils.CreatePackage("PackageZ", "1.0.0"); + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); await SimpleTestPackageUtility.CreateFolderFeedV3Async( pathContext.PackageSource, PackageSaveMode.Defaultv3, packageX, - packageZ); + packageY); string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs); - string whyCommandArgs = $"nuget why {project.ProjectPath} {packageZ.Id}"; + string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id} -f {projectFramework}"; // Act CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs); // Assert Assert.Equal(ExitCodes.Success, result.ExitCode); - Assert.Contains($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageZ.Id}'", result.AllOutput); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); } [Fact] diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index f89c58a26ac..008315cfec2 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -17,7 +17,7 @@ public class XPlatWhyTests private static MSBuildAPIUtility MsBuild => new MSBuildAPIUtility(new TestCommandOutputLogger()); [Fact] - public async void WhyCommand_SimpleTransitiveDependency_PathExists() + public async void WhyCommand_ProjectHasTransitiveDependency_DependencyPathExists() { // Arrange var logger = new TestCommandOutputLogger(); @@ -60,7 +60,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } [Fact] - public async void WhyCommand_SimpleTransitiveDependency_OutputFormatIsCorrect() + public async void WhyCommand_ProjectHasTransitiveDependency_OutputFormatIsCorrect() { // Arrange var logger = new Mock(); @@ -107,9 +107,8 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)\n", ConsoleColor.Cyan), Times.Exactly(1)); } - [Fact] - public async void WhyCommand_SimpleTransitiveDependency_PathDoesNotExist() + public async void WhyCommand_ProjectHasNoDependencyOnTargetPackage_PathDoesNotExist() { // Arrange var logger = new TestCommandOutputLogger(); @@ -150,7 +149,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } [Fact] - public void WhyCommand_SimpleTransitiveDependency_DidNotRunRestore_Fails() + public void WhyCommand_ProjectDidNotRunRestore_Fails() { // Arrange var logger = new TestCommandOutputLogger(); @@ -251,25 +250,5 @@ public void WhyCommand_InvalidProject_Fails() Assert.Equal(ExitCodes.InvalidArguments, result); Assert.Contains($"Unable to run 'dotnet nuget why'. Missing or invalid project/solution file 'FakeProjectPath.csproj'.", errorOutput); } - - /* - [Fact] - public void NewTest() - { - var assetsFilePath = Path.Combine(pathContext.SolutionRoot, "obj", LockFileFormat.AssetsFileName); - var format = new LockFileFormat(); - LockFile assetsFile = format.Read(assetsFilePath); - - var responses = new Dictionary - { - { testFeedUrl, ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.index.json", GetType()) }, - { query + "?q=nuget&skip=0&take=26&prerelease=true&semVerLevel=2.0.0", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.nugetSearchPage1.json", GetType()) }, - { query + "?q=nuget&skip=25&take=26&prerelease=true&semVerLevel=2.0.0", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.nugetSearchPage2.json", GetType()) }, - { query + "?q=&skip=0&take=26&prerelease=true&semVerLevel=2.0.0", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.blankSearchPage.json", GetType()) }, - { "https://api.nuget.org/v3/registration3-gz-semver2/nuget.core/index.json", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.nugetCoreIndex.json", GetType()) }, - { "https://api.nuget.org/v3/registration3-gz-semver2/microsoft.extensions.logging.abstractions/index.json", ProtocolUtility.GetResource("NuGet.PackageManagement.VisualStudio.Test.compiler.resources.loggingAbstractions.json", GetType()) } - }; - } - */ } } diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs deleted file mode 100644 index 3329a337155..00000000000 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// 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 NuGet.CommandLine.XPlat; -using Xunit; - -namespace NuGet.CommandLine.Xplat.Tests -{ - public class XPlatWhyUnitTests - { - [Theory] - [InlineData(true, false)] - [InlineData(false, true)] - public void ConfigPathsCommand_WithNullArguments_ThrowsArgumentNullException(bool argsIsNull, bool loggerIsNull) - { - // Arrange - ConfigPathsArgs args = null; - CommandOutputLogger getLogger = null; - - if (!argsIsNull) - { - args = new ConfigPathsArgs(); - } - - if (!loggerIsNull) - { - getLogger = new CommandOutputLogger(Common.LogLevel.Debug); - } - - // Act & Assert - Assert.Throws(() => ConfigPathsRunner.Run(args, loggerIsNull ? null : () => getLogger)); - } - - [Theory] - [InlineData(true, false)] - [InlineData(false, true)] - public void ConfigGetCommand_WithNullArguments_ThrowsArgumentNullException(bool argsIsNull, bool loggerIsNull) - { - // Arrange - ConfigGetArgs args = null; - CommandOutputLogger getLogger = null; - - if (!argsIsNull) - { - args = new ConfigGetArgs(); - } - - if (!loggerIsNull) - { - getLogger = new CommandOutputLogger(Common.LogLevel.Debug); - } - - // Act & Assert - Assert.Throws(() => ConfigGetRunner.Run(args, loggerIsNull ? null : () => getLogger)); - } - - [Theory] - [InlineData(true, false)] - [InlineData(false, true)] - public void ConfigSetCommand_WithNullArguments_ThrowsArgumentNullException(bool argsIsNull, bool loggerIsNull) - { - // Arrange - ConfigSetArgs args = null; - CommandOutputLogger getLogger = null; - - if (!argsIsNull) - { - args = new ConfigSetArgs(); - } - - if (!loggerIsNull) - { - getLogger = new CommandOutputLogger(Common.LogLevel.Debug); - } - - // Act & Assert - Assert.Throws(() => ConfigSetRunner.Run(args, loggerIsNull ? null : () => getLogger)); - } - - [Theory] - [InlineData(true, false)] - [InlineData(false, true)] - public void ConfigUnsetCommand_WithNullArguments_ThrowsArgumentNullException(bool argsIsNull, bool loggerIsNull) - { - // Arrange - ConfigUnsetArgs args = null; - CommandOutputLogger getLogger = null; - - if (!argsIsNull) - { - args = new ConfigUnsetArgs(); - } - - if (!loggerIsNull) - { - getLogger = new CommandOutputLogger(Common.LogLevel.Debug); - } - - // Act & Assert - Assert.Throws(() => ConfigUnsetRunner.Run(args, loggerIsNull ? null : () => getLogger)); - } - } -} From 5c3ad520829d9e6adac8ad5f0cae2eb7ec41ea8c Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Mon, 13 May 2024 07:08:44 -0700 Subject: [PATCH 34/53] cleaning up --- .../Commands/WhyCommand/WhyCommand.cs | 7 +++---- .../Commands/WhyCommand/WhyCommandRunner.cs | 6 ------ .../NuGet.CommandLine.XPlat.csproj | 4 ++-- src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs | 9 +++------ 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs index 99b8f9160d6..06f561bdf2a 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommand.cs @@ -13,7 +13,7 @@ internal static class WhyCommand private static CliArgument Path = new CliArgument("PROJECT|SOLUTION") { Description = Strings.WhyCommand_PathArgument_Description, - Arity = ArgumentArity.ExactlyOne // ZeroOrOne? + Arity = ArgumentArity.ExactlyOne }; private static CliArgument Package = new CliArgument("PACKAGE") @@ -54,9 +54,8 @@ internal static void Register(CliCommand rootCommand, Func get parseResult.GetValue(Frameworks), logger); - WhyCommandRunner.ExecuteCommand(whyCommandArgs); - - return 0; + int exitCode = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + return exitCode; } catch (ArgumentException ex) { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 4e27c598b5a..c5087590ccd 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -206,12 +206,6 @@ private static void FindAllDependencyGraphs( doesProjectHaveDependencyOnPackage = true; dependencyGraphPerFramework.Add(frameworkPackage.Framework, GetDependencyGraphPerFramework(frameworkPackage.TopLevelPackages, packageLibraries, targetPackage)); - - // Sanity check - if (dependencyGraphPerFramework[frameworkPackage.Framework] == null) - { - Console.WriteLine("WTF?"); - } } else { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj index ad4b8e0b41f..51082266367 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj @@ -22,9 +22,9 @@ - + diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index 5e86a527c1a..7e541703d78 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -39,7 +39,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log) { #if DEBUG // Uncomment the following when debugging. Also uncomment the PackageReference for Microsoft.Build.Locator. - try + /*try { // .NET JIT compiles one method at a time. If this method calls `MSBuildLocator` directly, the // try block is never entered if Microsoft.Build.Locator.dll can't be found. So, run it in a @@ -50,7 +50,7 @@ public static int MainInternal(string[] args, CommandOutputLogger log) { // MSBuildLocator is used only to enable Visual Studio debugging. // It's not needed when using a patched dotnet sdk, so it doesn't matter if it fails. - } + }*/ var debugNuGetXPlat = Environment.GetEnvironmentVariable("DEBUG_NUGET_XPLAT"); @@ -95,15 +95,12 @@ public static int MainInternal(string[] args, CommandOutputLogger log) PackageSearchCommand.Register(rootCommand, getHidePrefixLogger); ConfigCommand.Register(rootCommand, getHidePrefixLogger); WhyCommand.Register(rootCommand, getHidePrefixLogger); - // this won't show up in help for dotnet nuget * this way - // eg. `dotnet nuget blah` should show the help option for `dotnet nuget`, which should list all sub commands, - // but it won't enter this if block, so we won't see 'why' as a subcommand CancellationTokenSource tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(TimeSpan.FromMinutes(DotnetPackageSearchTimeOut)); + int exitCodeValue = 0; CliConfiguration config = new(rootCommand); ParseResult parseResult = rootCommand.Parse(args, config); - int exitCodeValue = 0; try { From 852edf0019e0efd030f1f0bf7528f483116f2b15 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 15 May 2024 05:59:23 -0700 Subject: [PATCH 35/53] register dotnet nuget/package * commands separately --- .../NuGet.CommandLine.XPlat/Program.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index 7e541703d78..7f99b66e309 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -74,7 +74,12 @@ public static int MainInternal(string[] args, CommandOutputLogger log) NuGet.Common.Migrations.MigrationRunner.Run(); - // Migrating from Microsoft.Extensions.CommandLineUtils.CommandLineApplication to System.Commandline.CliCommand + // TODO: Migrating from Microsoft.Extensions.CommandLineUtils.CommandLineApplication to System.Commandline.CliCommand + // If we are looking to add further commands here, we should also look to redesign this parsing logic at that time + // See related issues: + // - https://github.com/NuGet/Home/issues/11996 + // - https://github.com/NuGet/Home/issues/11997 + // - https://github.com/NuGet/Home/issues/13089 if ((args.Count() >= 2 && args[0] == "package" && args[1] == "search") || (args.Any() && args[0] == "config") || (args.Any() && args[0] == "why")) @@ -85,16 +90,20 @@ public static int MainInternal(string[] args, CommandOutputLogger log) return log; }; - CliCommand rootCommand = new CliCommand("package"); + CliCommand rootCommand; + if (args[0] == "package") + { + rootCommand = new CliCommand("package"); - if (args[0] == "why") + PackageSearchCommand.Register(rootCommand, getHidePrefixLogger); + } + else { rootCommand = new CliCommand("nuget"); - } - PackageSearchCommand.Register(rootCommand, getHidePrefixLogger); - ConfigCommand.Register(rootCommand, getHidePrefixLogger); - WhyCommand.Register(rootCommand, getHidePrefixLogger); + ConfigCommand.Register(rootCommand, getHidePrefixLogger); + WhyCommand.Register(rootCommand, getHidePrefixLogger); + } CancellationTokenSource tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(TimeSpan.FromMinutes(DotnetPackageSearchTimeOut)); From 923a1dfdfdc80b5b8000d720dca8aba482bae1fa Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 15 May 2024 10:56:49 -0700 Subject: [PATCH 36/53] moved print methods out to new class --- .../WhyCommand/WhyCommandPrintUtility.cs | 181 +++++++++++++++ .../Commands/WhyCommand/WhyCommandRunner.cs | 214 ++---------------- 2 files changed, 204 insertions(+), 191 deletions(-) create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs new file mode 100644 index 00000000000..84bf4c76226 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs @@ -0,0 +1,181 @@ +// 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.Linq; +using NuGet.Shared; + +namespace NuGet.CommandLine.XPlat +{ + internal static class WhyCommandPrintUtility + { + private const ConsoleColor TargetPackageColor = ConsoleColor.Cyan; + + // Dependency graph console output symbols + private const string ChildNodeSymbol = "├─ "; + private const string LastChildNodeSymbol = "└─ "; + + private const string ChildPrefixSymbol = "│ "; + private const string LastChildPrefixSymbol = " "; + + /// + /// Prints the dependency graphs for all target frameworks. + /// + /// A dictionary mapping target frameworks to their dependency graphs. + /// The package we want the dependency paths for. + public static void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework, string targetPackage, ILoggerWithColor logger) + { + // print empty line + logger.LogMinimal(""); + + // deduplicate the dependency graphs + List> deduplicatedFrameworks = GetDeduplicatedFrameworks(dependencyGraphPerFramework); + + foreach (var frameworks in deduplicatedFrameworks) + { + PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[frameworks.FirstOrDefault()], targetPackage, logger); + } + } + + /// + /// Prints the dependency graph for a given framework/list of frameworks. + /// + /// The list of frameworks that share this dependency graph. + /// The top-level package nodes of the dependency graph. + /// The package we want the dependency paths for. + private static void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes, string targetPackage, ILoggerWithColor logger) + { + // print framework header + foreach (var framework in frameworks) + { + logger.LogMinimal($" [{framework}]"); + } + + logger.LogMinimal($" {ChildPrefixSymbol}"); + + if (topLevelNodes == null || topLevelNodes.Count == 0) + { + logger.LogMinimal($" {LastChildNodeSymbol}{Strings.WhyCommand_Message_NoDependencyGraphsFoundForFramework}\n\n"); + return; + } + + var stack = new Stack(); + + // initialize the stack with all top-level nodes + int counter = 0; + foreach (var node in topLevelNodes) + { + stack.Push(new StackOutputData(node, prefix: " ", isLastChild: counter++ == 0)); + } + + // print the dependency graph + while (stack.Count > 0) + { + var current = stack.Pop(); + + string currentPrefix, childPrefix; + if (current.IsLastChild) + { + currentPrefix = current.Prefix + LastChildNodeSymbol; + childPrefix = current.Prefix + LastChildPrefixSymbol; + } + else + { + currentPrefix = current.Prefix + ChildNodeSymbol; + childPrefix = current.Prefix + ChildPrefixSymbol; + } + + // print current node + if (current.Node.Id == targetPackage) + { + logger.LogMinimal($"{currentPrefix}", Console.ForegroundColor); + logger.LogMinimal($"{current.Node.Id} (v{current.Node.Version})\n", TargetPackageColor); + } + else + { + logger.LogMinimal($"{currentPrefix}{current.Node.Id} (v{current.Node.Version})"); + } + + if (current.Node.Children?.Count > 0) + { + // push all the node's children onto the stack + counter = 0; + foreach (var child in current.Node.Children) + { + stack.Push(new StackOutputData(child, childPrefix, isLastChild: counter++ == 0)); + } + } + } + + logger.LogMinimal(""); + } + + /// + /// Deduplicates dependency graphs, and returns groups of frameworks that share the same graph. + /// + /// A dictionary mapping target frameworks to their dependency graphs. + /// + /// eg. { { "net6.0", "netcoreapp3.1" }, { "net472" } } + /// + private static List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) + { + List frameworksWithoutGraphs = null; + var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); + + foreach (var framework in dependencyGraphPerFramework.Keys) + { + if (dependencyGraphPerFramework[framework] == null) + { + frameworksWithoutGraphs ??= []; + frameworksWithoutGraphs.Add(framework); + continue; + } + + int hash = GetDependencyGraphHashCode(dependencyGraphPerFramework[framework]); + if (dependencyGraphHashes.ContainsKey(hash)) + { + dependencyGraphHashes[hash].Add(framework); + } + else + { + dependencyGraphHashes.Add(hash, [framework]); + } + } + + var deduplicatedFrameworks = dependencyGraphHashes.Values.ToList(); + + if (frameworksWithoutGraphs != null) + { + deduplicatedFrameworks.Add(frameworksWithoutGraphs); + } + + return deduplicatedFrameworks; + } + + /// + /// Returns a hash for a given dependency graph. Used to deduplicate dependency graphs for different frameworks. + /// + /// The dependency graph for a given framework. + private static int GetDependencyGraphHashCode(List graph) + { + var hashCodeCombiner = new HashCodeCombiner(); + hashCodeCombiner.AddUnorderedSequence(graph); + return hashCodeCombiner.CombinedHash; + } + + private class StackOutputData + { + public DependencyNode Node { get; set; } + public string Prefix { get; set; } + public bool IsLastChild { get; set; } + + public StackOutputData(DependencyNode node, string prefix, bool isLastChild) + { + Node = node; + Prefix = prefix; + IsLastChild = isLastChild; + } + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index c5087590ccd..189b3740d40 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -18,15 +18,6 @@ internal static class WhyCommandRunner private const string ProjectName = "MSBuildProjectName"; private const string ProjectAssetsFile = "ProjectAssetsFile"; - private const ConsoleColor TargetPackageColor = ConsoleColor.Cyan; - - // Dependency graph console output symbols - private const string ChildNodeSymbol = "├─ "; - private const string LastChildNodeSymbol = "└─ "; - - private const string ChildPrefixSymbol = "│ "; - private const string LastChildPrefixSymbol = " "; - /// /// Executes the 'why' command. /// @@ -230,7 +221,7 @@ private static void FindAllDependencyGraphs( project.GetPropertyValue(ProjectName), targetPackage)); - PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); + WhyCommandPrintUtility.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); } } else @@ -406,177 +397,6 @@ private static void AddToGraph( } } - /// - /// Prints the dependency graphs for all target frameworks. - /// - /// A dictionary mapping target frameworks to their dependency graphs. - /// The package we want the dependency paths for. - private static void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework, string targetPackage, ILoggerWithColor logger) - { - // print empty line - logger.LogMinimal(""); - - // deduplicate the dependency graphs - List> deduplicatedFrameworks = GetDeduplicatedFrameworks(dependencyGraphPerFramework); - - foreach (var frameworks in deduplicatedFrameworks) - { - PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[frameworks.FirstOrDefault()], targetPackage, logger); - } - } - - /// - /// Deduplicates dependency graphs, and returns groups of frameworks that share the same graph. - /// - /// A dictionary mapping target frameworks to their dependency graphs. - /// - /// eg. { { "net6.0", "netcoreapp3.1" }, { "net472" } } - /// - private static List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) - { - List frameworksWithoutGraphs = null; - var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); - - foreach (var framework in dependencyGraphPerFramework.Keys) - { - if (dependencyGraphPerFramework[framework] == null) - { - frameworksWithoutGraphs ??= []; - frameworksWithoutGraphs.Add(framework); - continue; - } - - int hash = GetDependencyGraphHashCode(dependencyGraphPerFramework[framework]); - if (dependencyGraphHashes.ContainsKey(hash)) - { - dependencyGraphHashes[hash].Add(framework); - } - else - { - dependencyGraphHashes.Add(hash, [framework]); - } - } - - var deduplicatedFrameworks = dependencyGraphHashes.Values.ToList(); - - if (frameworksWithoutGraphs != null) - { - deduplicatedFrameworks.Add(frameworksWithoutGraphs); - } - - return deduplicatedFrameworks; - } - - /// - /// Prints the dependency graph for a given framework/list of frameworks. - /// - /// The list of frameworks that share this dependency graph. - /// The top-level package nodes of the dependency graph. - /// The package we want the dependency paths for. - private static void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes, string targetPackage, ILoggerWithColor logger) - { - // print framework header - foreach (var framework in frameworks) - { - logger.LogMinimal($"\t[{framework}]"); - } - - logger.LogMinimal($"\t {ChildPrefixSymbol}"); - - if (topLevelNodes == null || topLevelNodes.Count == 0) - { - logger.LogMinimal($"\t {LastChildNodeSymbol}{Strings.WhyCommand_Message_NoDependencyGraphsFoundForFramework}\n\n"); - return; - } - - var stack = new Stack(); - - // initialize the stack with all top-level nodes - int counter = 0; - foreach (var node in topLevelNodes) - { - stack.Push(new StackOutputData(node, prefix: "\t ", isLastChild: counter++ == 0)); - } - - // print the dependency graph - while (stack.Count > 0) - { - var current = stack.Pop(); - - string currentPrefix, childPrefix; - if (current.IsLastChild) - { - currentPrefix = current.Prefix + LastChildNodeSymbol; - childPrefix = current.Prefix + LastChildPrefixSymbol; - } - else - { - currentPrefix = current.Prefix + ChildNodeSymbol; - childPrefix = current.Prefix + ChildPrefixSymbol; - } - - // print current node - if (current.Node.Id == targetPackage) - { - logger.LogMinimal($"{currentPrefix}", Console.ForegroundColor); - logger.LogMinimal($"{current.Node.Id} (v{current.Node.Version})\n", TargetPackageColor); - } - else - { - logger.LogMinimal($"{currentPrefix}{current.Node.Id} (v{current.Node.Version})"); - } - - if (current.Node.Children?.Count > 0) - { - // push all the node's children onto the stack - counter = 0; - foreach (var child in current.Node.Children) - { - stack.Push(new StackOutputData(child, childPrefix, isLastChild: counter++ == 0)); - } - } - } - - logger.LogMinimal(""); - } - - /// - /// Returns a hash for a given dependency graph. Used to deduplicate dependency graphs for different frameworks. - /// - /// The dependency graph for a given framework. - private static int GetDependencyGraphHashCode(List graph) - { - var hashCodeCombiner = new HashCodeCombiner(); - hashCodeCombiner.AddUnorderedSequence(graph); - return hashCodeCombiner.CombinedHash; - } - - /// - /// Represents a node in the package dependency graph. - /// - private class DependencyNode - { - public string Id { get; set; } - public string Version { get; set; } - public HashSet Children { get; set; } - - public DependencyNode(string id, string version) - { - Id = id; - Version = version; - Children = new HashSet(); - } - - public override int GetHashCode() - { - var hashCodeCombiner = new HashCodeCombiner(); - hashCodeCombiner.AddObject(Id); - hashCodeCombiner.AddObject(Version); - hashCodeCombiner.AddUnorderedSequence(Children); - return hashCodeCombiner.CombinedHash; - } - } - private class StackDependencyData { public string Id { get; set; } @@ -588,19 +408,31 @@ public StackDependencyData(string currentId, StackDependencyData parentDependenc Parent = parentDependencyData; } } + } + + /// + /// Represents a node in the package dependency graph. + /// + internal class DependencyNode + { + public string Id { get; set; } + public string Version { get; set; } + public HashSet Children { get; set; } - private class StackOutputData + public DependencyNode(string id, string version) { - public DependencyNode Node { get; set; } - public string Prefix { get; set; } - public bool IsLastChild { get; set; } + Id = id; + Version = version; + Children = new HashSet(); + } - public StackOutputData(DependencyNode node, string prefix, bool isLastChild) - { - Node = node; - Prefix = prefix; - IsLastChild = isLastChild; - } + public override int GetHashCode() + { + var hashCodeCombiner = new HashCodeCombiner(); + hashCodeCombiner.AddObject(Id); + hashCodeCombiner.AddObject(Version); + hashCodeCombiner.AddUnorderedSequence(Children); + return hashCodeCombiner.CombinedHash; } } } From 527d44850eb88fb71c123001682ae2ab25fd07c8 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 15 May 2024 16:46:43 -0700 Subject: [PATCH 37/53] wip: addressing feedback, trialing new method for top level packages/projects --- .../Commands/WhyCommand/WhyCommandArgs.cs | 2 + .../WhyCommand/WhyCommandPrintUtility.cs | 21 ++- .../Commands/WhyCommand/WhyCommandRunner.cs | 145 +++++++++++++----- .../Strings.Designer.cs | 23 +-- .../NuGet.CommandLine.XPlat/Strings.resx | 18 +-- 5 files changed, 133 insertions(+), 76 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs index 31ed44714d2..3cee48370af 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandArgs.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System; using System.Collections.Generic; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs index 84bf4c76226..d9eac49dad8 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -24,7 +26,7 @@ internal static class WhyCommandPrintUtility /// /// A dictionary mapping target frameworks to their dependency graphs. /// The package we want the dependency paths for. - public static void PrintAllDependencyGraphs(Dictionary> dependencyGraphPerFramework, string targetPackage, ILoggerWithColor logger) + public static void PrintAllDependencyGraphs(Dictionary?> dependencyGraphPerFramework, string targetPackage, ILoggerWithColor logger) { // print empty line logger.LogMinimal(""); @@ -34,7 +36,10 @@ public static void PrintAllDependencyGraphs(Dictionary 0) + { + PrintDependencyGraphPerFramework(frameworks, dependencyGraphPerFramework[frameworks.First()], targetPackage, logger); + } } } @@ -44,7 +49,7 @@ public static void PrintAllDependencyGraphs(DictionaryThe list of frameworks that share this dependency graph. /// The top-level package nodes of the dependency graph. /// The package we want the dependency paths for. - private static void PrintDependencyGraphPerFramework(List frameworks, List topLevelNodes, string targetPackage, ILoggerWithColor logger) + private static void PrintDependencyGraphPerFramework(List frameworks, List? topLevelNodes, string targetPackage, ILoggerWithColor logger) { // print framework header foreach (var framework in frameworks) @@ -64,7 +69,7 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li // initialize the stack with all top-level nodes int counter = 0; - foreach (var node in topLevelNodes) + foreach (var node in topLevelNodes.OrderByDescending(c => c.Id, StringComparer.CurrentCulture)) { stack.Push(new StackOutputData(node, prefix: " ", isLastChild: counter++ == 0)); } @@ -101,7 +106,7 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li { // push all the node's children onto the stack counter = 0; - foreach (var child in current.Node.Children) + foreach (var child in current.Node.Children.OrderByDescending(c => c.Id, StringComparer.CurrentCulture)) { stack.Push(new StackOutputData(child, childPrefix, isLastChild: counter++ == 0)); } @@ -118,9 +123,9 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li /// /// eg. { { "net6.0", "netcoreapp3.1" }, { "net472" } } /// - private static List> GetDeduplicatedFrameworks(Dictionary> dependencyGraphPerFramework) + private static List> GetDeduplicatedFrameworks(Dictionary?> dependencyGraphPerFramework) { - List frameworksWithoutGraphs = null; + List? frameworksWithoutGraphs = null; var dependencyGraphHashes = new Dictionary>(dependencyGraphPerFramework.Count); foreach (var framework in dependencyGraphPerFramework.Keys) @@ -157,7 +162,7 @@ private static List> GetDeduplicatedFrameworks(Dictionary /// The dependency graph for a given framework. - private static int GetDependencyGraphHashCode(List graph) + private static int GetDependencyGraphHashCode(List? graph) { var hashCodeCombiner = new HashCodeCombiner(); hashCodeCombiner.AddUnorderedSequence(graph); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 189b3740d40..7523b77f693 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + using System; using System.Collections.Generic; using System.Globalization; @@ -47,11 +49,11 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) foreach (var projectPath in projectPaths) { Project project = MSBuildAPIUtility.GetProject(projectPath); - LockFile assetsFile = GetProjectAssetsFile(project, whyCommandArgs.Logger); + LockFile? assetsFile = GetProjectAssetsFile(project, whyCommandArgs.Logger); if (assetsFile != null) { - FindAllDependencyGraphs(whyCommandArgs, msBuild, project, assetsFile); + OutputAllDependencyGraphs(whyCommandArgs, msBuild, project, assetsFile); } ProjectCollection.GlobalProjectCollection.UnloadProject(project); @@ -106,6 +108,23 @@ private static void ValidateFrameworksOption(List frameworks) string.Format(CultureInfo.CurrentCulture, Strings.WhyCommand_Error_InvalidFramework)); } + + // Break up the whole thing into a for loop instead of using linq, and then we can output the errors for each user input framework, + // OR + // Remove this validation completely, and get targets based on whatever inputs the user provides. + // Still need to validate that the user input 'target alias' exists in the assets file + /* + foreach (var framework in parsedFrameworks) + { + if (framework.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + string.Format(CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_InvalidFramework, + framework.GetShortFolderName())); + } + } + */ } /// @@ -113,8 +132,8 @@ private static void ValidateFrameworksOption(List frameworks) /// /// Evaluated MSBuild project /// Logger for the 'why' command - /// Assets file for given project - private static LockFile GetProjectAssetsFile(Project project, ILoggerWithColor logger) + /// Assets file for the given project. Returns null if there was any issue finding or parsing the assets file. + private static LockFile? GetProjectAssetsFile(Project project, ILoggerWithColor logger) { if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) { @@ -151,8 +170,9 @@ private static LockFile GetProjectAssetsFile(Project project, ILoggerWithColor l logger.LogError( string.Format( CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_CannotReadAssetsFile, - assetsPath)); + Strings.WhyCommand_Error_InvalidAssetsFile, + assetsFile.Path, + project.FullPath)); return null; } @@ -161,27 +181,28 @@ private static LockFile GetProjectAssetsFile(Project project, ILoggerWithColor l } /// - /// Finds dependency graphs for a given project, and prints output to the console. + /// Finds and prints dependency graphs for a given project. /// /// CLI arguments for the 'why' command /// MSBuild utility /// Current project /// Assets file for current project - private static void FindAllDependencyGraphs( + private static void OutputAllDependencyGraphs( WhyCommandArgs whyCommandArgs, MSBuildAPIUtility msBuild, Project project, LockFile assetsFile) { + bool doesProjectHaveDependencyOnPackage = false; + string targetPackage = whyCommandArgs.Package; + var dependencyGraphPerFramework = new Dictionary?>(assetsFile.Targets.Count); + // get all resolved package references for a project List frameworkPackages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true, includeProjectReferences: true); + Dictionary> topLevelReferences = GetTopLevelPackageAndProjectReferences(assetsFile, whyCommandArgs.Frameworks, project.DirectoryPath); if (frameworkPackages?.Count > 0) { - string targetPackage = whyCommandArgs.Package; - bool doesProjectHaveDependencyOnPackage = false; - var dependencyGraphPerFramework = new Dictionary>(assetsFile.Targets.Count); - foreach (var frameworkPackage in frameworkPackages) { LockFileTarget target = assetsFile.GetTarget(frameworkPackage.Framework, runtimeIdentifier: null); @@ -204,32 +225,25 @@ private static void FindAllDependencyGraphs( } } } + } - if (!doesProjectHaveDependencyOnPackage) - { - whyCommandArgs.Logger.LogMinimal( - string.Format( - Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, - project.GetPropertyValue(ProjectName), - targetPackage)); - } - else - { - whyCommandArgs.Logger.LogMinimal( - string.Format( - Strings.WhyCommand_Message_DependencyGraphsFoundInProject, - project.GetPropertyValue(ProjectName), - targetPackage)); - - WhyCommandPrintUtility.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); - } + if (!doesProjectHaveDependencyOnPackage) + { + whyCommandArgs.Logger.LogMinimal( + string.Format( + Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, + project.GetPropertyValue(ProjectName), + targetPackage)); } else { whyCommandArgs.Logger.LogMinimal( - string.Format( - Strings.WhyCommand_Message_NoPackagesFoundForGivenFrameworks, - project.GetPropertyValue(ProjectName))); + string.Format( + Strings.WhyCommand_Message_DependencyGraphsFoundInProject, + project.GetPropertyValue(ProjectName), + targetPackage)); + + WhyCommandPrintUtility.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); } } @@ -240,12 +254,12 @@ private static void FindAllDependencyGraphs( /// All package libraries for a given framework. /// The package we want the dependency paths for. /// List of all top-level package nodes in the dependency graph. - private static List GetDependencyGraphPerFramework( + private static List? GetDependencyGraphPerFramework( IEnumerable topLevelPackages, IList packageLibraries, string targetPackage) { - List dependencyGraph = null; + List? dependencyGraph = null; // hashset tracking every package node that we've traversed var visited = new HashSet(); @@ -257,7 +271,7 @@ private static List GetDependencyGraphPerFramework( foreach (var topLevelPackage in topLevelPackages) { // use depth-first search to find dependency paths from the top-level package to the target package - DependencyNode topLevelNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, visited, dependencyNodes, versions, targetPackage); + DependencyNode? topLevelNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, visited, dependencyNodes, versions, targetPackage); if (topLevelNode != null) { @@ -280,7 +294,10 @@ private static Dictionary GetAllResolvedVersions(IList GetAllResolvedVersions(IListDictionary mapping packageIds to their resolved versions. /// The package we want the dependency paths for. /// The top-level package node in the dependency graph (if a path was found), or null (if no path was found) - private static DependencyNode FindDependencyPath( + private static DependencyNode? FindDependencyPath( string topLevelPackage, IList packageLibraries, HashSet visited, @@ -365,7 +382,7 @@ private static void AddToGraph( { // first, we traverse the target's parents, listing the packages in the path from the target to the top-level package var dependencyPath = new List { targetPackageData.Id }; - StackDependencyData current = targetPackageData.Parent; + StackDependencyData? current = targetPackageData.Parent; while (current != null) { @@ -397,12 +414,58 @@ private static void AddToGraph( } } + private static Dictionary> GetTopLevelPackageAndProjectReferences(LockFile assetsFile, List userInputFrameworks, string projectDirectoryPath) + { + var topLevelReferences = new Dictionary>(); + + var targetAliases = assetsFile.PackageSpec.RestoreMetadata.OriginalTargetFrameworks; + + if (userInputFrameworks?.Count > 0) + { + targetAliases = targetAliases.Where(f => userInputFrameworks.Contains(f)).ToList(); + } + + var projectLibraries = assetsFile.Libraries.Where(l => l.Type == "project"); + var projectLibraryPathToName = new Dictionary(projectLibraries.Count()); + + foreach (var library in projectLibraries) + { + projectLibraryPathToName.Add(Path.GetFullPath(library.Path, projectDirectoryPath), library.Name); + } + + foreach (string targetAlias in targetAliases) + { + topLevelReferences.Add(targetAlias, []); + + // top level packages + TargetFrameworkInformation? targetFrameworkInformation = assetsFile.PackageSpec.TargetFrameworks.FirstOrDefault(tfi => tfi.TargetAlias.Equals(targetAlias, StringComparison.OrdinalIgnoreCase)); + if (targetFrameworkInformation != default) + { + var topLevelPackages = targetFrameworkInformation.Dependencies.Select(d => d.Name); + topLevelReferences[targetAlias].AddRange(topLevelPackages); + } + + // top level projects + ProjectRestoreMetadataFrameworkInfo? restoreMetadataFrameworkInfo = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks.FirstOrDefault(tfi => tfi.TargetAlias.Equals(targetAlias, StringComparison.OrdinalIgnoreCase)); + if (restoreMetadataFrameworkInfo != default) + { + var topLevelProjectPaths = restoreMetadataFrameworkInfo.ProjectReferences.Select(p => p.ProjectPath); + foreach (var projectPath in topLevelProjectPaths) + { + topLevelReferences[targetAlias].Add(projectLibraryPathToName[projectPath]); + } + } + } + + return topLevelReferences; + } + private class StackDependencyData { public string Id { get; set; } - public StackDependencyData Parent { get; set; } + public StackDependencyData? Parent { get; set; } - public StackDependencyData(string currentId, StackDependencyData parentDependencyData) + public StackDependencyData(string currentId, StackDependencyData? parentDependencyData) { Id = currentId; Parent = parentDependencyData; diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index b3343d1b30c..b1657b04210 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -2206,7 +2206,7 @@ internal static string Warning_HttpServerUsage_MultipleSources { } /// - /// Looks up a localized string similar to Lists the dependency graph for a particular package for a given project or solution.. + /// Looks up a localized string similar to Shows the dependency graph for a particular package for a given project or solution.. /// internal static string WhyCommand_Description { get { @@ -2224,16 +2224,16 @@ internal static string WhyCommand_Error_ArgumentCannotBeEmpty { } /// - /// Looks up a localized string similar to Unable to read the assets file `{0}`.. + /// Looks up a localized string similar to The assets file {0} is invalid. Please run restore for project '{1}' before running this command.. /// - internal static string WhyCommand_Error_CannotReadAssetsFile { + internal static string WhyCommand_Error_InvalidAssetsFile { get { - return ResourceManager.GetString("WhyCommand_Error_CannotReadAssetsFile", resourceCulture); + return ResourceManager.GetString("WhyCommand_Error_InvalidAssetsFile", resourceCulture); } } /// - /// Looks up a localized string similar to Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. For a list of accepted values, please visit https://learn.microsoft.com/en-us/nuget/reference/target-frameworks#supported-frameworks.. + /// Looks up a localized string similar to Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. For a list of accepted values, please visit https://learn.microsoft.com/en-us/nuget/reference/target-frameworks#supported-frameworks. /// internal static string WhyCommand_Error_InvalidFramework { get { @@ -2269,7 +2269,7 @@ internal static string WhyCommand_Message_DependencyGraphsFoundInProject { } /// - /// Looks up a localized string similar to No dependency graph(s) found.. + /// Looks up a localized string similar to No dependency graph(s) found for this target framework.. /// internal static string WhyCommand_Message_NoDependencyGraphsFoundForFramework { get { @@ -2278,7 +2278,7 @@ internal static string WhyCommand_Message_NoDependencyGraphsFoundForFramework { } /// - /// Looks up a localized string similar to Project '{0}' does not have any dependency graph(s) for '{1}'.. + /// Looks up a localized string similar to Project '{0}' does not have a dependency on '{1}'.. /// internal static string WhyCommand_Message_NoDependencyGraphsFoundInProject { get { @@ -2286,15 +2286,6 @@ internal static string WhyCommand_Message_NoDependencyGraphsFoundInProject { } } - /// - /// Looks up a localized string similar to No packages were found for the project '{0}' for the given framework(s).. - /// - internal static string WhyCommand_Message_NoPackagesFoundForGivenFrameworks { - get { - return ResourceManager.GetString("WhyCommand_Message_NoPackagesFoundForGivenFrameworks", resourceCulture); - } - } - /// /// Looks up a localized string similar to The package name to lookup in the dependency graph.. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 10fe3061a14..e51b90d75fc 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -913,7 +913,7 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Gets the NuGet configuration settings that will be applied. - Lists the dependency graph for a particular package for a given project or solution. + Shows the dependency graph for a particular package for a given project or solution. A path to a project, solution file, or directory. @@ -933,18 +933,14 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT {0} - Project/solution file path - Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. For a list of accepted values, please visit https://learn.microsoft.com/en-us/nuget/reference/target-frameworks#supported-frameworks. + Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. For a list of accepted values, please visit https://learn.microsoft.com/en-us/nuget/reference/target-frameworks#supported-frameworks - - Unable to read the assets file `{0}`. - {0} - Assets file path - - - No packages were found for the project '{0}' for the given framework(s). - {0} - Project name + + The assets file {0} is invalid. Please run restore for project '{1}' before running this command. + {0} - Assets file path, {1} - Project name - Project '{0}' does not have any dependency graph(s) for '{1}'. + Project '{0}' does not have a dependency on '{1}'. {0} - Project name, {1} - Target package @@ -952,6 +948,6 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT {0} - Project name, {1} - Target package - No dependency graph(s) found. + No dependency graph(s) found for this target framework. From 506ee92f6bb6c9c1d27316209988e272cee88c9c Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 16 May 2024 08:02:41 -0700 Subject: [PATCH 38/53] refactoring --- .../Commands/WhyCommand/DependencyNode.cs | 36 ++ .../Commands/WhyCommand/WhyCommandRunner.cs | 378 +----------------- .../DependencyGraphFinder.cs | 310 ++++++++++++++ .../DependencyGraphPrinter.cs} | 4 +- 4 files changed, 369 insertions(+), 359 deletions(-) create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/{WhyCommandPrintUtility.cs => WhyCommandUtility/DependencyGraphPrinter.cs} (98%) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs new file mode 100644 index 00000000000..ef17aeff821 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Generic; +using NuGet.Shared; + +namespace NuGet.CommandLine.XPlat +{ + /// + /// Represents a node in the package dependency graph. + /// + internal class DependencyNode + { + public string Id { get; set; } + public string Version { get; set; } + public HashSet Children { get; set; } + + public DependencyNode(string id, string version) + { + Id = id; + Version = version; + Children = new HashSet(); + } + + public override int GetHashCode() + { + var hashCodeCombiner = new HashCodeCombiner(); + hashCodeCombiner.AddObject(Id); + hashCodeCombiner.AddObject(Version); + hashCodeCombiner.AddUnorderedSequence(Children); + return hashCodeCombiner.CombinedHash; + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 7523b77f693..1af714a59f5 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -9,9 +9,8 @@ using System.IO; using System.Linq; using Microsoft.Build.Evaluation; -using NuGet.Frameworks; +using NuGet.CommandLine.XPlat.WhyCommandUtility; using NuGet.ProjectModel; -using NuGet.Shared; namespace NuGet.CommandLine.XPlat { @@ -30,7 +29,6 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) { ValidatePathArgument(whyCommandArgs.Path); ValidatePackageArgument(whyCommandArgs.Package); - ValidateFrameworksOption(whyCommandArgs.Frameworks); } catch (ArgumentException ex) { @@ -38,8 +36,6 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) return ExitCodes.InvalidArguments; } - var msBuild = new MSBuildAPIUtility(whyCommandArgs.Logger); - string targetPackage = whyCommandArgs.Package; IEnumerable projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") @@ -53,7 +49,26 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) if (assetsFile != null) { - OutputAllDependencyGraphs(whyCommandArgs, msBuild, project, assetsFile); + Dictionary?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphs(whyCommandArgs, assetsFile, project.DirectoryPath); + + if (dependencyGraphPerFramework != null) + { + whyCommandArgs.Logger.LogMinimal( + string.Format( + Strings.WhyCommand_Message_DependencyGraphsFoundInProject, + project.GetPropertyValue(ProjectName), + targetPackage)); + + DependencyGraphPrinter.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); + } + else + { + whyCommandArgs.Logger.LogMinimal( + string.Format( + Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, + project.GetPropertyValue(ProjectName), + targetPackage)); + } } ProjectCollection.GlobalProjectCollection.UnloadProject(project); @@ -94,39 +109,6 @@ private static void ValidatePackageArgument(string package) } } - private static void ValidateFrameworksOption(List frameworks) - { - var parsedFrameworks = frameworks.Select(f => - NuGetFramework.Parse( - f.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .ToArray()[0])); - - if (parsedFrameworks.Any(f => f.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_InvalidFramework)); - } - - // Break up the whole thing into a for loop instead of using linq, and then we can output the errors for each user input framework, - // OR - // Remove this validation completely, and get targets based on whatever inputs the user provides. - // Still need to validate that the user input 'target alias' exists in the assets file - /* - foreach (var framework in parsedFrameworks) - { - if (framework.Framework.Equals("Unsupported", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_InvalidFramework, - framework.GetShortFolderName())); - } - } - */ - } - /// /// Validates and returns the assets file for the given project. /// @@ -179,323 +161,5 @@ private static void ValidateFrameworksOption(List frameworks) return assetsFile; } - - /// - /// Finds and prints dependency graphs for a given project. - /// - /// CLI arguments for the 'why' command - /// MSBuild utility - /// Current project - /// Assets file for current project - private static void OutputAllDependencyGraphs( - WhyCommandArgs whyCommandArgs, - MSBuildAPIUtility msBuild, - Project project, - LockFile assetsFile) - { - bool doesProjectHaveDependencyOnPackage = false; - string targetPackage = whyCommandArgs.Package; - var dependencyGraphPerFramework = new Dictionary?>(assetsFile.Targets.Count); - - // get all resolved package references for a project - List frameworkPackages = msBuild.GetResolvedVersions(project, whyCommandArgs.Frameworks, assetsFile, transitive: true, includeProjectReferences: true); - Dictionary> topLevelReferences = GetTopLevelPackageAndProjectReferences(assetsFile, whyCommandArgs.Frameworks, project.DirectoryPath); - - if (frameworkPackages?.Count > 0) - { - foreach (var frameworkPackage in frameworkPackages) - { - LockFileTarget target = assetsFile.GetTarget(frameworkPackage.Framework, runtimeIdentifier: null); - - if (target != default) - { - // get all package libraries for the framework - IList packageLibraries = target.Libraries; - - // if the project has a dependency on the target package, get the dependency graph - if (packageLibraries.Any(l => l.Name == targetPackage)) - { - doesProjectHaveDependencyOnPackage = true; - dependencyGraphPerFramework.Add(frameworkPackage.Framework, - GetDependencyGraphPerFramework(frameworkPackage.TopLevelPackages, packageLibraries, targetPackage)); - } - else - { - dependencyGraphPerFramework.Add(frameworkPackage.Framework, null); - } - } - } - } - - if (!doesProjectHaveDependencyOnPackage) - { - whyCommandArgs.Logger.LogMinimal( - string.Format( - Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, - project.GetPropertyValue(ProjectName), - targetPackage)); - } - else - { - whyCommandArgs.Logger.LogMinimal( - string.Format( - Strings.WhyCommand_Message_DependencyGraphsFoundInProject, - project.GetPropertyValue(ProjectName), - targetPackage)); - - WhyCommandPrintUtility.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); - } - } - - /// - /// Finds all dependency paths from the top-level packages to the target package for a given framework. - /// - /// All top-level packages for the framework. - /// All package libraries for a given framework. - /// The package we want the dependency paths for. - /// List of all top-level package nodes in the dependency graph. - private static List? GetDependencyGraphPerFramework( - IEnumerable topLevelPackages, - IList packageLibraries, - string targetPackage) - { - List? dependencyGraph = null; - - // hashset tracking every package node that we've traversed - var visited = new HashSet(); - // dictionary tracking all package nodes that have been added to the graph, mapped to their DependencyNode objects - var dependencyNodes = new Dictionary(); - // dictionary mapping all packageIds to their resolved version - Dictionary versions = GetAllResolvedVersions(packageLibraries); - - foreach (var topLevelPackage in topLevelPackages) - { - // use depth-first search to find dependency paths from the top-level package to the target package - DependencyNode? topLevelNode = FindDependencyPath(topLevelPackage.Name, packageLibraries, visited, dependencyNodes, versions, targetPackage); - - if (topLevelNode != null) - { - dependencyGraph ??= []; - dependencyGraph.Add(topLevelNode); - } - } - - return dependencyGraph; - } - - - /// - /// Adds all resolved versions of packages to a dictionary. - /// - /// All package libraries for a given framework. - private static Dictionary GetAllResolvedVersions(IList packageLibraries) - { - var versions = new Dictionary(); - - foreach (var package in packageLibraries) - { - if (package?.Name != null && package?.Version != null) - { - versions.Add(package.Name, package.Version.ToNormalizedString()); - } - } - - return versions; - } - - /// - /// Traverses the dependency graph for a given top-level package, looking for a path to the target package. - /// - /// Top-level package. - /// All package libraries for a given framework. - /// HashSet tracking every package node that we've traversed. - /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. - /// Dictionary mapping packageIds to their resolved versions. - /// The package we want the dependency paths for. - /// The top-level package node in the dependency graph (if a path was found), or null (if no path was found) - private static DependencyNode? FindDependencyPath( - string topLevelPackage, - IList packageLibraries, - HashSet visited, - Dictionary dependencyNodes, - Dictionary versions, - string targetPackage) - { - var stack = new Stack(); - stack.Push(new StackDependencyData(topLevelPackage, null)); - - while (stack.Count > 0) - { - var currentPackageData = stack.Pop(); - var currentPackageId = currentPackageData.Id; - - // if we reach the target node, or if we've already traversed this node and found dependency paths, add it to the graph - if (currentPackageId == targetPackage - || dependencyNodes.ContainsKey(currentPackageId)) - { - AddToGraph(currentPackageData, dependencyNodes, versions); - continue; - } - - // if we have already traversed this node's children, continue - if (visited.Contains(currentPackageId)) - { - continue; - } - - visited.Add(currentPackageId); - - // get all dependencies for the current package - var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackageId)?.Dependencies; - - if (dependencies?.Count > 0) - { - // push all the dependencies onto the stack - foreach (var dependency in dependencies) - { - stack.Push(new StackDependencyData(dependency.Id, currentPackageData)); - } - } - } - - if (dependencyNodes.ContainsKey(topLevelPackage)) - { - return dependencyNodes[topLevelPackage]; - } - else - { - return null; - } - } - - /// - /// Adds a dependency path to the graph, starting from the target package and traversing up to the top-level package. - /// - /// Target node data. This stores parent references, so it can be used to construct the dependency graph - /// up to the top-level package. - /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. - /// Dictionary mapping packageIds to their resolved versions. - private static void AddToGraph( - StackDependencyData targetPackageData, - Dictionary dependencyNodes, - Dictionary versions) - { - // first, we traverse the target's parents, listing the packages in the path from the target to the top-level package - var dependencyPath = new List { targetPackageData.Id }; - StackDependencyData? current = targetPackageData.Parent; - - while (current != null) - { - dependencyPath.Add(current.Id); - current = current.Parent; - } - - // then, we traverse this list from the target package to the top-level package, initializing/updating their dependency nodes as needed - for (int i = 0; i < dependencyPath.Count; i++) - { - string currentPackageId = dependencyPath[i]; - - if (!dependencyNodes.ContainsKey(currentPackageId)) - { - dependencyNodes.Add(currentPackageId, new DependencyNode(currentPackageId, versions[currentPackageId])); - } - - if (i > 0) - { - var childNode = dependencyNodes[dependencyPath[i - 1]]; - - if (dependencyNodes[currentPackageId].Children.Any(p => p.Id == childNode.Id)) - { - continue; - } - - dependencyNodes[currentPackageId].Children.Add(childNode); - } - } - } - - private static Dictionary> GetTopLevelPackageAndProjectReferences(LockFile assetsFile, List userInputFrameworks, string projectDirectoryPath) - { - var topLevelReferences = new Dictionary>(); - - var targetAliases = assetsFile.PackageSpec.RestoreMetadata.OriginalTargetFrameworks; - - if (userInputFrameworks?.Count > 0) - { - targetAliases = targetAliases.Where(f => userInputFrameworks.Contains(f)).ToList(); - } - - var projectLibraries = assetsFile.Libraries.Where(l => l.Type == "project"); - var projectLibraryPathToName = new Dictionary(projectLibraries.Count()); - - foreach (var library in projectLibraries) - { - projectLibraryPathToName.Add(Path.GetFullPath(library.Path, projectDirectoryPath), library.Name); - } - - foreach (string targetAlias in targetAliases) - { - topLevelReferences.Add(targetAlias, []); - - // top level packages - TargetFrameworkInformation? targetFrameworkInformation = assetsFile.PackageSpec.TargetFrameworks.FirstOrDefault(tfi => tfi.TargetAlias.Equals(targetAlias, StringComparison.OrdinalIgnoreCase)); - if (targetFrameworkInformation != default) - { - var topLevelPackages = targetFrameworkInformation.Dependencies.Select(d => d.Name); - topLevelReferences[targetAlias].AddRange(topLevelPackages); - } - - // top level projects - ProjectRestoreMetadataFrameworkInfo? restoreMetadataFrameworkInfo = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks.FirstOrDefault(tfi => tfi.TargetAlias.Equals(targetAlias, StringComparison.OrdinalIgnoreCase)); - if (restoreMetadataFrameworkInfo != default) - { - var topLevelProjectPaths = restoreMetadataFrameworkInfo.ProjectReferences.Select(p => p.ProjectPath); - foreach (var projectPath in topLevelProjectPaths) - { - topLevelReferences[targetAlias].Add(projectLibraryPathToName[projectPath]); - } - } - } - - return topLevelReferences; - } - - private class StackDependencyData - { - public string Id { get; set; } - public StackDependencyData? Parent { get; set; } - - public StackDependencyData(string currentId, StackDependencyData? parentDependencyData) - { - Id = currentId; - Parent = parentDependencyData; - } - } - } - - /// - /// Represents a node in the package dependency graph. - /// - internal class DependencyNode - { - public string Id { get; set; } - public string Version { get; set; } - public HashSet Children { get; set; } - - public DependencyNode(string id, string version) - { - Id = id; - Version = version; - Children = new HashSet(); - } - - public override int GetHashCode() - { - var hashCodeCombiner = new HashCodeCombiner(); - hashCodeCombiner.AddObject(Id); - hashCodeCombiner.AddObject(Version); - hashCodeCombiner.AddUnorderedSequence(Children); - return hashCodeCombiner.CombinedHash; - } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs new file mode 100644 index 00000000000..a761cfc7fe2 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs @@ -0,0 +1,310 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NuGet.ProjectModel; + +namespace NuGet.CommandLine.XPlat.WhyCommandUtility +{ + internal static class DependencyGraphFinder + { + /// + /// Finds all dependency graphs for a given project. + /// + /// CLI arguments for the 'why' command. + /// Assets file for the project. + /// Root directory of the project. + /// + /// Dictionary mapping target framework aliases to their respective dependency graphs. + /// Returns null if the project does not have a dependency on the target package. + /// + public static Dictionary?>? GetAllDependencyGraphs( + WhyCommandArgs whyCommandArgs, + LockFile assetsFile, + string projectDirectoryPath) + { + var dependencyGraphPerFramework = new Dictionary?>(assetsFile.Targets.Count); + + bool doesProjectHaveDependencyOnPackage = false; + string targetPackage = whyCommandArgs.Package; + + // get all top-level package and project references for the project, categorized by target framework alias + Dictionary> topLevelReferencesByFramework = GetTopLevelPackageAndProjectReferences(assetsFile, whyCommandArgs.Frameworks, projectDirectoryPath); + + if (topLevelReferencesByFramework.Count > 0) + { + foreach (var (targetFrameworkAlias, topLevelReferences) in topLevelReferencesByFramework) + { + LockFileTarget target = assetsFile.GetTarget(targetFrameworkAlias, runtimeIdentifier: null); + + if (target != null) + { + // get all package libraries for the framework + IList packageLibraries = target.Libraries; + + // if the project has a dependency on the target package, get the dependency graph + if (packageLibraries.Any(l => l.Name == targetPackage)) + { + doesProjectHaveDependencyOnPackage = true; + dependencyGraphPerFramework.Add(targetFrameworkAlias, + GetDependencyGraphPerFramework(topLevelReferences, packageLibraries, targetPackage)); + } + else + { + dependencyGraphPerFramework.Add(targetFrameworkAlias, null); + } + } + } + } + + return doesProjectHaveDependencyOnPackage + ? dependencyGraphPerFramework + : null; + } + + /// + /// Finds all dependency paths from the top-level packages to the target package for a given framework. + /// + /// All top-level package and project references for the framework. + /// All package libraries for the framework. + /// The package we want the dependency paths for. + /// + /// List of all top-level package nodes in the dependency graph. + /// + private static List? GetDependencyGraphPerFramework( + List topLevelReferences, + IList packageLibraries, + string targetPackage) + { + List? dependencyGraph = null; + + // hashset tracking every package node that we've traversed + var visited = new HashSet(); + // dictionary tracking all package nodes that have been added to the graph, mapped to their DependencyNode objects + var dependencyNodes = new Dictionary(); + // dictionary mapping all packageIds to their resolved version + Dictionary versions = GetAllResolvedVersions(packageLibraries); + + foreach (var topLevelReference in topLevelReferences) + { + // use depth-first search to find dependency paths from the top-level package to the target package + DependencyNode? topLevelNode = FindDependencyPath(topLevelReference, packageLibraries, visited, dependencyNodes, versions, targetPackage); + + if (topLevelNode != null) + { + dependencyGraph ??= []; + dependencyGraph.Add(topLevelNode); + } + } + + return dependencyGraph; + } + + /// + /// Traverses the dependency graph for a given top-level package, looking for a path to the target package. + /// + /// Top-level package. + /// All package libraries for a given framework. + /// HashSet tracking every package node that we've traversed. + /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. + /// Dictionary mapping packageIds to their resolved versions. + /// The package we want the dependency paths for. + /// + /// The top-level package node in the dependency graph (if a path was found), or null (if no path was found). + /// + private static DependencyNode? FindDependencyPath( + string topLevelPackage, + IList packageLibraries, + HashSet visited, + Dictionary dependencyNodes, + Dictionary versions, + string targetPackage) + { + var stack = new Stack(); + stack.Push(new StackDependencyData(topLevelPackage, null)); + + while (stack.Count > 0) + { + var currentPackageData = stack.Pop(); + var currentPackageId = currentPackageData.Id; + + // if we reach the target node, or if we've already traversed this node and found dependency paths, add it to the graph + if (currentPackageId == targetPackage + || dependencyNodes.ContainsKey(currentPackageId)) + { + AddToGraph(currentPackageData, dependencyNodes, versions); + continue; + } + + // if we have already traversed this node's children, continue + if (visited.Contains(currentPackageId)) + { + continue; + } + + visited.Add(currentPackageId); + + // get all dependencies for the current package + var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackageId)?.Dependencies; + + if (dependencies?.Count > 0) + { + // push all the dependencies onto the stack + foreach (var dependency in dependencies) + { + stack.Push(new StackDependencyData(dependency.Id, currentPackageData)); + } + } + } + + if (dependencyNodes.ContainsKey(topLevelPackage)) + { + return dependencyNodes[topLevelPackage]; + } + else + { + return null; + } + } + + /// + /// Adds a dependency path to the graph, starting from the target package and traversing up to the top-level package. + /// + /// Target node data. This stores parent references, so it can be used to construct the dependency graph + /// up to the top-level package. + /// Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects. + /// Dictionary mapping packageIds to their resolved versions. + private static void AddToGraph( + StackDependencyData targetPackageData, + Dictionary dependencyNodes, + Dictionary versions) + { + // first, we traverse the target's parents, listing the packages in the path from the target to the top-level package + var dependencyPath = new List { targetPackageData.Id }; + StackDependencyData? current = targetPackageData.Parent; + + while (current != null) + { + dependencyPath.Add(current.Id); + current = current.Parent; + } + + // then, we traverse this list from the target package to the top-level package, initializing/updating their dependency nodes as needed + for (int i = 0; i < dependencyPath.Count; i++) + { + string currentPackageId = dependencyPath[i]; + + if (!dependencyNodes.ContainsKey(currentPackageId)) + { + dependencyNodes.Add(currentPackageId, new DependencyNode(currentPackageId, versions[currentPackageId])); + } + + if (i > 0) + { + var childNode = dependencyNodes[dependencyPath[i - 1]]; + + if (dependencyNodes[currentPackageId].Children.Any(p => p.Id == childNode.Id)) + { + continue; + } + + dependencyNodes[currentPackageId].Children.Add(childNode); + } + } + } + + /// + /// Get all top-level package and project references for the given project. + /// + /// Assets file for the project. + /// List of target framework aliases. + /// Root directory of the project. + /// + /// Dictionary mapping the project's target framework aliases to their respective top-level package and project references. + /// + private static Dictionary> GetTopLevelPackageAndProjectReferences( + LockFile assetsFile, + List userInputFrameworks, + string projectDirectoryPath) + { + var topLevelReferences = new Dictionary>(); + + var targetAliases = assetsFile.PackageSpec.RestoreMetadata.OriginalTargetFrameworks; + + if (userInputFrameworks?.Count > 0) + { + targetAliases = targetAliases.Where(f => userInputFrameworks.Contains(f)).ToList(); + } + + var projectLibraries = assetsFile.Libraries.Where(l => l.Type == "project"); + var projectLibraryPathToName = new Dictionary(projectLibraries.Count()); + + foreach (var library in projectLibraries) + { + projectLibraryPathToName.Add(Path.GetFullPath(library.Path, projectDirectoryPath), library.Name); + } + + foreach (string targetAlias in targetAliases) + { + topLevelReferences.Add(targetAlias, []); + + // top level packages + TargetFrameworkInformation? targetFrameworkInformation = assetsFile.PackageSpec.TargetFrameworks.FirstOrDefault(tfi => tfi.TargetAlias.Equals(targetAlias, StringComparison.OrdinalIgnoreCase)); + if (targetFrameworkInformation != default) + { + var topLevelPackages = targetFrameworkInformation.Dependencies.Select(d => d.Name); + topLevelReferences[targetAlias].AddRange(topLevelPackages); + } + + // top level projects + ProjectRestoreMetadataFrameworkInfo? restoreMetadataFrameworkInfo = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks.FirstOrDefault(tfi => tfi.TargetAlias.Equals(targetAlias, StringComparison.OrdinalIgnoreCase)); + if (restoreMetadataFrameworkInfo != default) + { + var topLevelProjectPaths = restoreMetadataFrameworkInfo.ProjectReferences.Select(p => p.ProjectPath); + foreach (var projectPath in topLevelProjectPaths) + { + topLevelReferences[targetAlias].Add(projectLibraryPathToName[projectPath]); + } + } + } + + return topLevelReferences; + } + + /// + /// Adds all resolved versions of packages to a dictionary. + /// + /// All package libraries for a given framework. + private static Dictionary GetAllResolvedVersions(IList packageLibraries) + { + var versions = new Dictionary(); + + foreach (var package in packageLibraries) + { + if (package?.Name != null && package?.Version != null) + { + versions.Add(package.Name, package.Version.ToNormalizedString()); + } + } + + return versions; + } + + private class StackDependencyData + { + public string Id { get; set; } + public StackDependencyData? Parent { get; set; } + + public StackDependencyData(string currentId, StackDependencyData? parentDependencyData) + { + Id = currentId; + Parent = parentDependencyData; + } + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs similarity index 98% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs index d9eac49dad8..a51301405e3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandPrintUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs @@ -8,9 +8,9 @@ using System.Linq; using NuGet.Shared; -namespace NuGet.CommandLine.XPlat +namespace NuGet.CommandLine.XPlat.WhyCommandUtility { - internal static class WhyCommandPrintUtility + internal static class DependencyGraphPrinter { private const ConsoleColor TargetPackageColor = ConsoleColor.Cyan; From 129710505b25e8eedca3c6c73f7c60203966fc48 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 16 May 2024 08:20:40 -0700 Subject: [PATCH 39/53] reverted MSBuildAPIUtility changes --- .../Utility/MSBuildAPIUtility.cs | 50 ++----------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs index a1ed3df5fc6..167d6311cc1 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Text; using Microsoft.Build.Construction; @@ -657,11 +656,10 @@ internal static bool IsPackageReferenceProject(Project project) /// A list of frameworks /// Assets file for all targets and libraries /// Include transitive packages/projects in the result - /// Include project references in the result /// FrameworkPackages collection with top-level and transitive package/project /// references for each framework, or null on error internal List GetResolvedVersions( - Project project, IEnumerable userInputFrameworks, LockFile assetsFile, bool transitive, bool includeProjectReferences = false) + Project project, IEnumerable userInputFrameworks, LockFile assetsFile, bool transitive) { if (userInputFrameworks == null) { @@ -722,14 +720,10 @@ internal List GetResolvedVersions( // Find the tfminformation corresponding to the target to // get the top-level dependencies TargetFrameworkInformation tfmInformation; - IEnumerable projectReferences; try { tfmInformation = requestedTargetFrameworks.First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)); - projectReferences = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks - .First(tfm => tfm.FrameworkName.Equals(target.TargetFramework)) - .ProjectReferences.Select(p => p.ProjectPath); } catch (Exception) { @@ -747,19 +741,11 @@ internal List GetResolvedVersions( var matchingPackages = frameworkDependencies.Where(d => d.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase)).ToList(); - List matchingProjects = null; - if (library.Type == "project" && includeProjectReferences) - { - string projectLibraryPath = GetProjectLibraryPath(library, assetsFile.Libraries, project.DirectoryPath); - matchingProjects ??= projectReferences.Where(p => - p.Equals(projectLibraryPath, StringComparison.OrdinalIgnoreCase)).ToList(); - } - var resolvedVersion = library.Version.ToString(); //In case we found a matching package in requestedVersions, the package will be //top level. - if (matchingPackages.Count > 0) + if (matchingPackages.Any()) { var topLevelPackage = matchingPackages.Single(); InstalledPackageReference installedPackage = default; @@ -807,24 +793,11 @@ internal List GetResolvedVersions( installedPackage.AutoReference = topLevelPackage.AutoReferenced; - if (library.Type != "project" || includeProjectReferences) + if (library.Type != "project") { topLevelPackages.Add(installedPackage); } } - else if (matchingProjects?.Count > 0) - { - var topLevelProject = matchingProjects.Single(); - var projectReference = new InstalledPackageReference(library.Name) - { - OriginalRequestedVersion = resolvedVersion - }; - - if (includeProjectReferences) - { - topLevelPackages.Add(projectReference); - } - } // If no matching packages were found, then the package is transitive, // and include-transitive must be used to add the package else if (transitive) // be sure to exclude "project" references here as these are irrelevant @@ -836,7 +809,7 @@ internal List GetResolvedVersions( .Build() }; - if (library.Type != "project" || includeProjectReferences) + if (library.Type != "project") { transitivePackages.Add(installedPackage); } @@ -854,21 +827,6 @@ internal List GetResolvedVersions( return resultPackages; } - /// - /// Gets the absolute path of the given project library. - /// - /// The target project library that we want the path for. - /// All libraries in the assets file. - /// The absolute path to the project base directory. - /// The absolute path of the matching project library. - private string GetProjectLibraryPath(LockFileTargetLibrary library, IList libraries, string projectDirectoryPath) - { - string relativePath = libraries.Where(l => l.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase) - && l.Version.Equals(library.Version)) - .FirstOrDefault().Path; - - return Path.GetFullPath(relativePath, projectDirectoryPath); - } /// /// Returns all package references after evaluating the condition on the item groups. From fec9de831e22f5f51352d82de1ddc15dbe772dd1 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 16 May 2024 08:26:05 -0700 Subject: [PATCH 40/53] nit --- .../WhyCommandUtility/DependencyGraphFinder.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs index a761cfc7fe2..1d2b99e693e 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs @@ -162,14 +162,9 @@ internal static class DependencyGraphFinder } } - if (dependencyNodes.ContainsKey(topLevelPackage)) - { - return dependencyNodes[topLevelPackage]; - } - else - { - return null; - } + return dependencyNodes.ContainsKey(topLevelPackage) + ? dependencyNodes[topLevelPackage] + : null; } /// From b164b31e53165e664d4011d2caeeeca1cf70fd29 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 16 May 2024 08:35:15 -0700 Subject: [PATCH 41/53] fixed tests (at least locally) --- .../Dotnet.Integration.Test/DotnetWhyTests.cs | 2 +- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index df0f2d0136f..16908e03a84 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -84,7 +84,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( // Assert Assert.Equal(ExitCodes.Success, result.ExitCode); - Assert.Contains($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageZ.Id}'", result.AllOutput); + Assert.Contains($"Project '{ProjectName}' does not have a dependency on '{packageZ.Id}'", result.AllOutput); } [Fact] diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index 008315cfec2..4e8cc76bf2f 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -100,10 +100,10 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( logger.Verify(x => x.LogMinimal("Project 'Test.Project.DotnetNugetWhy' has the following dependency graph(s) for 'PackageY':"), Times.Exactly(1)); logger.Verify(x => x.LogMinimal(""), Times.Exactly(2)); - logger.Verify(x => x.LogMinimal(" [net472]"), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" │ "), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" └─ PackageX (v1.0.0)"), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" └─ ", ConsoleColor.Gray), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" [net472]"), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" │ "), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" └─ PackageX (v1.0.0)"), Times.Exactly(1)); + logger.Verify(x => x.LogMinimal(" └─ ", ConsoleColor.Gray), Times.Exactly(1)); logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)\n", ConsoleColor.Cyan), Times.Exactly(1)); } @@ -145,7 +145,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( var output = logger.ShowMessages(); Assert.Equal(ExitCodes.Success, result); - Assert.Contains($"Project '{ProjectName}' does not have any dependency graph(s) for '{packageZ.Id}'", output); + Assert.Contains($"Project '{ProjectName}' does not have a dependency on '{packageZ.Id}'", output); } [Fact] From 2a1c57668720c51cb994c4c4468a6283148db3e0 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 16 May 2024 10:38:27 -0700 Subject: [PATCH 42/53] whitespace changes? --- .../Commands/WhyCommand/DependencyNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs index ef17aeff821..a3953de00af 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #nullable enable From dccf34c522aa02169dfb3844c24c8230850a1594 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 16 May 2024 15:39:57 -0700 Subject: [PATCH 43/53] added unit tests, fixed func tests, + some small nits --- .../Commands/WhyCommand/WhyCommandRunner.cs | 5 +- .../DependencyGraphFinder.cs | 20 +- .../Dotnet.Integration.Test/DotnetWhyTests.cs | 16 +- .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 50 +- .../NuGet.CommandLine.Xplat.Tests.csproj | 8 + .../XPlatWhyUnitTests.cs | 147 ++ ...NW.Test.SampleProject1.project.assets.json | 1629 +++++++++++++++++ 7 files changed, 1809 insertions(+), 66 deletions(-) create mode 100644 test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs create mode 100644 test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/compiler/resources/DNW.Test.SampleProject1.project.assets.json diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 1af714a59f5..64b8a19b803 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -49,7 +49,10 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) if (assetsFile != null) { - Dictionary?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphs(whyCommandArgs, assetsFile, project.DirectoryPath); + Dictionary?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphs( + assetsFile, + whyCommandArgs.Package, + whyCommandArgs.Frameworks); if (dependencyGraphPerFramework != null) { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs index 1d2b99e693e..1663c4acb55 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs @@ -16,25 +16,23 @@ internal static class DependencyGraphFinder /// /// Finds all dependency graphs for a given project. /// - /// CLI arguments for the 'why' command. /// Assets file for the project. - /// Root directory of the project. + /// The package we want the dependency paths for. + /// List of target framework aliases. /// /// Dictionary mapping target framework aliases to their respective dependency graphs. /// Returns null if the project does not have a dependency on the target package. /// public static Dictionary?>? GetAllDependencyGraphs( - WhyCommandArgs whyCommandArgs, LockFile assetsFile, - string projectDirectoryPath) + string targetPackage, + List userInputFrameworks) { var dependencyGraphPerFramework = new Dictionary?>(assetsFile.Targets.Count); - bool doesProjectHaveDependencyOnPackage = false; - string targetPackage = whyCommandArgs.Package; // get all top-level package and project references for the project, categorized by target framework alias - Dictionary> topLevelReferencesByFramework = GetTopLevelPackageAndProjectReferences(assetsFile, whyCommandArgs.Frameworks, projectDirectoryPath); + Dictionary> topLevelReferencesByFramework = GetTopLevelPackageAndProjectReferences(assetsFile, userInputFrameworks, assetsFile.PackageSpec.BaseDirectory); if (topLevelReferencesByFramework.Count > 0) { @@ -231,11 +229,14 @@ private static Dictionary> GetTopLevelPackageAndProjectRefe var targetAliases = assetsFile.PackageSpec.RestoreMetadata.OriginalTargetFrameworks; + // filter the targets to the set of targets that the user has specified if (userInputFrameworks?.Count > 0) { targetAliases = targetAliases.Where(f => userInputFrameworks.Contains(f)).ToList(); } + // we need to match top-level project references to their target library entries using their paths, + // so we will store all project reference paths in a dictionary here var projectLibraries = assetsFile.Libraries.Where(l => l.Type == "project"); var projectLibraryPathToName = new Dictionary(projectLibraries.Count()); @@ -244,11 +245,12 @@ private static Dictionary> GetTopLevelPackageAndProjectRefe projectLibraryPathToName.Add(Path.GetFullPath(library.Path, projectDirectoryPath), library.Name); } + // get all top-level references for each target alias foreach (string targetAlias in targetAliases) { topLevelReferences.Add(targetAlias, []); - // top level packages + // top-level packages TargetFrameworkInformation? targetFrameworkInformation = assetsFile.PackageSpec.TargetFrameworks.FirstOrDefault(tfi => tfi.TargetAlias.Equals(targetAlias, StringComparison.OrdinalIgnoreCase)); if (targetFrameworkInformation != default) { @@ -256,7 +258,7 @@ private static Dictionary> GetTopLevelPackageAndProjectRefe topLevelReferences[targetAlias].AddRange(topLevelPackages); } - // top level projects + // top-level projects ProjectRestoreMetadataFrameworkInfo? restoreMetadataFrameworkInfo = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks.FirstOrDefault(tfi => tfi.TargetAlias.Equals(targetAlias, StringComparison.OrdinalIgnoreCase)); if (restoreMetadataFrameworkInfo != default) { diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index 16908e03a84..cca1b5a329a 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -29,8 +29,8 @@ public async void WhyCommand_ProjectHasTransitiveDependency_DependencyPathExists var projectFramework = "net472"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1", projectFramework); packageX.Dependencies.Add(packageY); @@ -63,10 +63,10 @@ public async void WhyCommand_ProjectHasNoDependencyOnTargetPackage_PathDoesNotEx var projectFramework = "net472"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); project.AddPackageToFramework(projectFramework, packageX); - var packageZ = XPlatTestUtils.CreatePackage("PackageZ", "1.0.0"); + var packageZ = XPlatTestUtils.CreatePackage("PackageZ", "1.0.0", projectFramework); await SimpleTestPackageUtility.CreateFolderFeedV3Async( pathContext.PackageSource, @@ -95,8 +95,8 @@ public async void WhyCommand_WithFrameworksOption_OptionParsedSuccessfully() var projectFramework = "net472"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1", projectFramework); packageX.Dependencies.Add(packageY); @@ -129,8 +129,8 @@ public async void WhyCommand_WithFrameworksOptionAlias_OptionParsedSuccessfully( var projectFramework = "net472"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1", projectFramework); packageX.Dependencies.Add(packageY); diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index 4e8cc76bf2f..7020ba830e4 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -57,54 +57,8 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( Assert.Equal(ExitCodes.Success, result); Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); - } - - [Fact] - public async void WhyCommand_ProjectHasTransitiveDependency_OutputFormatIsCorrect() - { - // Arrange - var logger = new Mock(); - - var pathContext = new SimpleTestPathContext(); - var projectFramework = "net472"; - var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); - - var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0"); - var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1"); - - packageX.Dependencies.Add(packageY); - - project.AddPackageToFramework(projectFramework, packageX); - - await SimpleTestPackageUtility.CreateFolderFeedV3Async( - pathContext.PackageSource, - PackageSaveMode.Defaultv3, - packageX, - packageY); - - var addPackageArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); - var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); - var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageArgs, MsBuild); - - var whyCommandArgs = new WhyCommandArgs( - project.ProjectPath, - packageY.Id, - [projectFramework], - logger.Object); - - // Act - var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); - - // Assert - Assert.Equal(ExitCodes.Success, result); - - logger.Verify(x => x.LogMinimal("Project 'Test.Project.DotnetNugetWhy' has the following dependency graph(s) for 'PackageY':"), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(""), Times.Exactly(2)); - logger.Verify(x => x.LogMinimal(" [net472]"), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" │ "), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" └─ PackageX (v1.0.0)"), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal(" └─ ", ConsoleColor.Gray), Times.Exactly(1)); - logger.Verify(x => x.LogMinimal("PackageY (v1.0.1)\n", ConsoleColor.Cyan), Times.Exactly(1)); + Assert.Contains($"{packageX.Id} (v{packageX.Version})", output); + Assert.Contains($"{packageY.Id} (v{packageY.Version})", output); } [Fact] diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj index 490700276cb..b54bf812816 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/NuGet.CommandLine.Xplat.Tests.csproj @@ -21,4 +21,12 @@ + + + + + + + + diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs new file mode 100644 index 00000000000..3cdf2442378 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs @@ -0,0 +1,147 @@ +// 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 System.Linq; +using NuGet.CommandLine.XPlat.WhyCommandUtility; +using NuGet.ProjectModel; +using Test.Utility; +using Xunit; + +namespace NuGet.CommandLine.Xplat.Tests +{ + public class XPlatWhyUnitTests + { + [Fact] + public void WhyCommand_DependencyGraphFinder_MultipleDependencyPathsForTargetPackage_AllPathsFound() + { + // Arrange + var lockFileFormat = new LockFileFormat(); + var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); + var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + + string targetPackage = "System.Text.Json"; + var frameworks = new List(); + + // Act + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + + // Assert + + // direct dependency on target package is found + Assert.Contains(dependencyGraphs["net472"], dep => (dep.Id == "System.Text.Json") && (dep.Version == "8.0.0")); + + // transitive dependency from a top-level package is found, with the correct resolved version + Assert.Contains(dependencyGraphs["net472"].First(dep => dep.Id == "Azure.Core").Children, dep => (dep.Id == "System.Text.Json") && (dep.Version == "8.0.0")); + + // transitive dependency from a top-level project reference is found, with the correct resolved version + Assert.Contains(dependencyGraphs["net472"].First(dep => dep.Id == "DotnetNuGetWhyPackage").Children, dep => (dep.Id == "System.Text.Json") && (dep.Version == "8.0.0")); + } + + [Fact] + public void WhyCommand_DependencyGraphFinder_DependencyOnTargetProject_AllPathsFound() + { + // Arrange + var lockFileFormat = new LockFileFormat(); + var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); + var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + + string targetPackage = "DotnetNuGetWhyPackage"; // project reference + var frameworks = new List(); + + // Act + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + + // Assert + + // direct dependency on target project reference is found + Assert.Contains(dependencyGraphs["net472"], dep => dep.Id == "DotnetNuGetWhyPackage"); + + // transitive dependency on target project reference is found + Assert.Contains(dependencyGraphs["net472"].First(dep => dep.Id == "CustomProjectName").Children, dep => dep.Id == "DotnetNuGetWhyPackage"); + } + + [Fact] + public void WhyCommand_DependencyGraphFinder_NoDependencyOnTargetPackage_ReturnsNullGraph() + { + // Arrange + var lockFileFormat = new LockFileFormat(); + var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); + var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + + string targetPackage = "NotARealPackage"; + var frameworks = new List(); + + // Act + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + + // Assert + + // no paths found for any framework + Assert.Null(dependencyGraphs); + } + + [Fact] + public void WhyCommand_DependencyGraphFinder_DependencyOnTargetPackageForOnlyOneFramework_ReturnsCorrectGraphs() + { + // Arrange + var lockFileFormat = new LockFileFormat(); + var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); + var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + + string targetPackage = "Azure.Core"; + var frameworks = new List(); + + // Act + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + + // Assert + + // paths found for one framework + Assert.Contains(dependencyGraphs["net472"], dep => dep.Id == "Azure.Core"); + + // no paths found for the other framework + Assert.Null(dependencyGraphs["net6.0"]); + } + + [Fact] + public void WhyCommand_DependencyGraphFinder_DependencyOnTargetPackage_FrameworkOptionSpecified_PathIsFound() + { + // Arrange + var lockFileFormat = new LockFileFormat(); + var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); + var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + + string targetPackage = "Azure.Core"; + List frameworks = ["net472"]; + + // Act + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + + // Assert + + // Path found + Assert.Contains(dependencyGraphs["net472"], dep => dep.Id == "Azure.Core"); + } + + [Fact] + public void WhyCommand_DependencyGraphFinder_DependencyOnTargetPackageForOnlyOneFramework_DifferentFrameworkSpecified_ReturnsNullGraph() + { + // Arrange + var lockFileFormat = new LockFileFormat(); + var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); + var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + + string targetPackage = "Azure.Core"; + List frameworks = ["net6.0"]; + + // Act + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + + // Assert + + // no paths found + Assert.Null(dependencyGraphs); + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/compiler/resources/DNW.Test.SampleProject1.project.assets.json b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/compiler/resources/DNW.Test.SampleProject1.project.assets.json new file mode 100644 index 00000000000..5113a5be6fa --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/compiler/resources/DNW.Test.SampleProject1.project.assets.json @@ -0,0 +1,1629 @@ +{ + "version": 3, + "targets": { + ".NETFramework,Version=v4.7.2": { + "Azure.Core/1.38.0": { + "type": "package", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "1.1.1", + "System.ClientModel": "1.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.1", + "System.Memory.Data": "1.0.2", + "System.Numerics.Vectors": "4.5.0", + "System.Text.Encodings.Web": "4.7.2", + "System.Text.Json": "4.7.2", + "System.Threading.Tasks.Extensions": "4.5.4" + }, + "frameworkAssemblies": [ + "System.Net.Http" + ], + "compile": { + "lib/net472/Azure.Core.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net472/Azure.Core.dll": { + "related": ".xml" + } + } + }, + "Microsoft.Bcl.AsyncInterfaces/8.0.0": { + "type": "package", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + }, + "compile": { + "lib/net462/Microsoft.Bcl.AsyncInterfaces.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net462/Microsoft.Bcl.AsyncInterfaces.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net462/_._": {} + } + }, + "Microsoft.CSharp/4.0.1": { + "type": "package", + "frameworkAssemblies": [ + "Microsoft.CSharp" + ], + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "Serilog/3.1.0": { + "type": "package", + "dependencies": { + "System.Diagnostics.DiagnosticSource": "7.0.2" + }, + "frameworkAssemblies": [ + "Microsoft.CSharp", + "System", + "System.Core" + ], + "compile": { + "lib/net471/Serilog.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net471/Serilog.dll": { + "related": ".xml" + } + } + }, + "System.Buffers/4.5.1": { + "type": "package", + "frameworkAssemblies": [ + "mscorlib" + ], + "compile": { + "ref/net45/System.Buffers.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net461/System.Buffers.dll": { + "related": ".xml" + } + } + }, + "System.ClientModel/1.0.0": { + "type": "package", + "dependencies": { + "System.Memory.Data": "1.0.2", + "System.Text.Json": "4.7.2" + }, + "compile": { + "lib/netstandard2.0/System.ClientModel.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/System.ClientModel.dll": { + "related": ".xml" + } + } + }, + "System.Diagnostics.DiagnosticSource/7.0.2": { + "type": "package", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + }, + "compile": { + "lib/net462/System.Diagnostics.DiagnosticSource.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net462/System.Diagnostics.DiagnosticSource.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net462/_._": {} + } + }, + "System.Drawing.Common/4.7.1": { + "type": "package", + "frameworkAssemblies": [ + "System", + "System.Drawing", + "mscorlib" + ], + "compile": { + "ref/net461/System.Drawing.Common.dll": {} + }, + "runtime": { + "lib/net461/System.Drawing.Common.dll": {} + } + }, + "System.Memory/4.5.5": { + "type": "package", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + }, + "frameworkAssemblies": [ + "System", + "mscorlib" + ], + "compile": { + "lib/net461/System.Memory.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net461/System.Memory.dll": { + "related": ".xml" + } + } + }, + "System.Memory.Data/1.0.2": { + "type": "package", + "dependencies": { + "System.Text.Encodings.Web": "4.7.2", + "System.Text.Json": "4.6.0" + }, + "compile": { + "lib/net461/System.Memory.Data.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net461/System.Memory.Data.dll": { + "related": ".xml" + } + } + }, + "System.Numerics.Vectors/4.5.0": { + "type": "package", + "frameworkAssemblies": [ + "System.Numerics", + "mscorlib" + ], + "compile": { + "ref/net46/System.Numerics.Vectors.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net46/System.Numerics.Vectors.dll": { + "related": ".xml" + } + } + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": { + "type": "package", + "frameworkAssemblies": [ + "mscorlib" + ], + "compile": { + "lib/net461/System.Runtime.CompilerServices.Unsafe.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net461/System.Runtime.CompilerServices.Unsafe.dll": { + "related": ".xml" + } + } + }, + "System.Text.Encodings.Web/8.0.0": { + "type": "package", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + }, + "compile": { + "lib/net462/System.Text.Encodings.Web.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net462/System.Text.Encodings.Web.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net462/_._": {} + } + }, + "System.Text.Json/8.0.0": { + "type": "package", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "8.0.0", + "System.Buffers": "4.5.1", + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "8.0.0", + "System.Threading.Tasks.Extensions": "4.5.4", + "System.ValueTuple": "4.5.0" + }, + "compile": { + "lib/net462/System.Text.Json.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net462/System.Text.Json.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net462/System.Text.Json.targets": {} + } + }, + "System.Threading.Tasks.Extensions/4.5.4": { + "type": "package", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + }, + "frameworkAssemblies": [ + "mscorlib" + ], + "compile": { + "lib/net461/System.Threading.Tasks.Extensions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net461/System.Threading.Tasks.Extensions.dll": { + "related": ".xml" + } + } + }, + "System.ValueTuple/4.5.0": { + "type": "package", + "frameworkAssemblies": [ + "mscorlib" + ], + "compile": { + "ref/net47/System.ValueTuple.dll": {} + }, + "runtime": { + "lib/net47/System.ValueTuple.dll": { + "related": ".xml" + } + } + }, + "CustomProjectName/1.0.0": { + "type": "project", + "framework": ".NETFramework,Version=v4.7.2", + "dependencies": { + "DotnetNuGetWhyPackage": "1.0.0" + }, + "compile": { + "bin/placeholder/CustomProjectName.dll": {} + }, + "runtime": { + "bin/placeholder/CustomProjectName.dll": {} + } + }, + "DotnetNuGetWhyPackage/1.0.0": { + "type": "project", + "framework": ".NETFramework,Version=v4.7.2", + "dependencies": { + "Azure.Core": "1.38.0", + "Microsoft.CSharp": "4.0.1", + "Serilog": "3.1.0", + "System.Drawing.Common": "4.7.1", + "System.Text.Json": "8.0.3" + }, + "compile": { + "bin/placeholder/DotnetNuGetWhyPackage.dll": {} + }, + "runtime": { + "bin/placeholder/DotnetNuGetWhyPackage.dll": {} + } + } + }, + "net6.0": { + "Microsoft.NETCore.Platforms/3.1.3": { + "type": "package", + "compile": { + "lib/netstandard1.0/_._": {} + }, + "runtime": { + "lib/netstandard1.0/_._": {} + } + }, + "Microsoft.Win32.SystemEvents/4.7.0": { + "type": "package", + "dependencies": { + "Microsoft.NETCore.Platforms": "3.1.0" + }, + "compile": { + "ref/netstandard2.0/_._": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll": { + "related": ".xml" + } + }, + "runtimeTargets": { + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll": { + "assetType": "runtime", + "rid": "win" + } + } + }, + "Newtonsoft.Json/13.0.3": { + "type": "package", + "compile": { + "lib/net6.0/Newtonsoft.Json.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "related": ".xml" + } + } + }, + "NuGet.Common/6.9.1": { + "type": "package", + "dependencies": { + "NuGet.Frameworks": "6.9.1" + }, + "compile": { + "lib/netstandard2.0/NuGet.Common.dll": {} + }, + "runtime": { + "lib/netstandard2.0/NuGet.Common.dll": {} + } + }, + "NuGet.Configuration/6.9.1": { + "type": "package", + "dependencies": { + "NuGet.Common": "6.9.1", + "System.Security.Cryptography.ProtectedData": "4.4.0" + }, + "compile": { + "lib/netstandard2.0/NuGet.Configuration.dll": {} + }, + "runtime": { + "lib/netstandard2.0/NuGet.Configuration.dll": {} + } + }, + "NuGet.Frameworks/6.9.1": { + "type": "package", + "compile": { + "lib/netstandard2.0/NuGet.Frameworks.dll": {} + }, + "runtime": { + "lib/netstandard2.0/NuGet.Frameworks.dll": {} + } + }, + "NuGet.Packaging/6.9.1": { + "type": "package", + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "NuGet.Configuration": "6.9.1", + "NuGet.Versioning": "6.9.1", + "System.Security.Cryptography.Pkcs": "6.0.4" + }, + "compile": { + "lib/net5.0/NuGet.Packaging.dll": {} + }, + "runtime": { + "lib/net5.0/NuGet.Packaging.dll": {} + } + }, + "NuGet.Protocol/6.9.1": { + "type": "package", + "dependencies": { + "NuGet.Packaging": "6.9.1" + }, + "compile": { + "lib/net5.0/NuGet.Protocol.dll": {} + }, + "runtime": { + "lib/net5.0/NuGet.Protocol.dll": {} + } + }, + "NuGet.Resolver/6.9.1": { + "type": "package", + "dependencies": { + "NuGet.Protocol": "6.9.1" + }, + "compile": { + "lib/net5.0/NuGet.Resolver.dll": {} + }, + "runtime": { + "lib/net5.0/NuGet.Resolver.dll": {} + } + }, + "NuGet.Versioning/6.9.1": { + "type": "package", + "compile": { + "lib/netstandard2.0/NuGet.Versioning.dll": {} + }, + "runtime": { + "lib/netstandard2.0/NuGet.Versioning.dll": {} + } + }, + "Serilog/3.1.0": { + "type": "package", + "compile": { + "lib/net6.0/Serilog.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/Serilog.dll": { + "related": ".xml" + } + } + }, + "System.Drawing.Common/4.7.1": { + "type": "package", + "dependencies": { + "Microsoft.NETCore.Platforms": "3.1.3", + "Microsoft.Win32.SystemEvents": "4.7.0" + }, + "compile": { + "ref/netcoreapp3.0/System.Drawing.Common.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/System.Drawing.Common.dll": {} + }, + "runtimeTargets": { + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "assetType": "runtime", + "rid": "unix" + }, + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll": { + "assetType": "runtime", + "rid": "win" + } + } + }, + "System.Formats.Asn1/6.0.0": { + "type": "package", + "compile": { + "lib/net6.0/System.Formats.Asn1.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/System.Formats.Asn1.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/netcoreapp3.1/_._": {} + } + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": { + "type": "package", + "compile": { + "lib/net6.0/System.Runtime.CompilerServices.Unsafe.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/System.Runtime.CompilerServices.Unsafe.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/netcoreapp3.1/_._": {} + } + }, + "System.Security.Cryptography.Pkcs/6.0.4": { + "type": "package", + "dependencies": { + "System.Formats.Asn1": "6.0.0" + }, + "compile": { + "lib/net6.0/System.Security.Cryptography.Pkcs.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/System.Security.Cryptography.Pkcs.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/netcoreapp3.1/_._": {} + }, + "runtimeTargets": { + "runtimes/win/lib/net6.0/System.Security.Cryptography.Pkcs.dll": { + "assetType": "runtime", + "rid": "win" + } + } + }, + "System.Security.Cryptography.ProtectedData/4.4.0": { + "type": "package", + "compile": { + "ref/netstandard2.0/System.Security.Cryptography.ProtectedData.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/System.Security.Cryptography.ProtectedData.dll": {} + }, + "runtimeTargets": { + "runtimes/win/lib/netstandard2.0/System.Security.Cryptography.ProtectedData.dll": { + "assetType": "runtime", + "rid": "win" + } + } + }, + "System.Text.Encodings.Web/8.0.0": { + "type": "package", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + }, + "compile": { + "lib/net6.0/System.Text.Encodings.Web.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/System.Text.Encodings.Web.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net6.0/_._": {} + }, + "runtimeTargets": { + "runtimes/browser/lib/net6.0/System.Text.Encodings.Web.dll": { + "assetType": "runtime", + "rid": "browser" + } + } + }, + "System.Text.Json/8.0.0": { + "type": "package", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "8.0.0" + }, + "compile": { + "lib/net6.0/System.Text.Json.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/System.Text.Json.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net6.0/System.Text.Json.targets": {} + } + }, + "CustomProjectName/1.0.0": { + "type": "project", + "framework": ".NETCoreApp,Version=v6.0", + "dependencies": { + "DotnetNuGetWhyPackage": "1.0.0" + }, + "compile": { + "bin/placeholder/CustomProjectName.dll": {} + }, + "runtime": { + "bin/placeholder/CustomProjectName.dll": {} + } + }, + "DotnetNuGetWhyPackage/1.0.0": { + "type": "project", + "framework": ".NETCoreApp,Version=v6.0", + "dependencies": { + "NuGet.Resolver": "6.9.1", + "Serilog": "3.1.0", + "System.Drawing.Common": "4.7.1", + "System.Text.Json": "8.0.3" + }, + "compile": { + "bin/placeholder/DotnetNuGetWhyPackage.dll": {} + }, + "runtime": { + "bin/placeholder/DotnetNuGetWhyPackage.dll": {} + } + } + } + }, + "libraries": { + "Azure.Core/1.38.0": { + "sha512": "IuEgCoVA0ef7E4pQtpC3+TkPbzaoQfa77HlfJDmfuaJUCVJmn7fT0izamZiryW5sYUFKizsftIxMkXKbgIcPMQ==", + "type": "package", + "path": "azure.core/1.38.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "CHANGELOG.md", + "README.md", + "azure.core.1.38.0.nupkg.sha512", + "azure.core.nuspec", + "azureicon.png", + "lib/net461/Azure.Core.dll", + "lib/net461/Azure.Core.xml", + "lib/net472/Azure.Core.dll", + "lib/net472/Azure.Core.xml", + "lib/net6.0/Azure.Core.dll", + "lib/net6.0/Azure.Core.xml", + "lib/netstandard2.0/Azure.Core.dll", + "lib/netstandard2.0/Azure.Core.xml" + ] + }, + "Microsoft.Bcl.AsyncInterfaces/8.0.0": { + "sha512": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==", + "type": "package", + "path": "microsoft.bcl.asyncinterfaces/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/Microsoft.Bcl.AsyncInterfaces.targets", + "buildTransitive/net462/_._", + "lib/net462/Microsoft.Bcl.AsyncInterfaces.dll", + "lib/net462/Microsoft.Bcl.AsyncInterfaces.xml", + "lib/netstandard2.0/Microsoft.Bcl.AsyncInterfaces.dll", + "lib/netstandard2.0/Microsoft.Bcl.AsyncInterfaces.xml", + "lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll", + "lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.xml", + "microsoft.bcl.asyncinterfaces.8.0.0.nupkg.sha512", + "microsoft.bcl.asyncinterfaces.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "Microsoft.CSharp/4.0.1": { + "sha512": "17h8b5mXa87XYKrrVqdgZ38JefSUqLChUQpXgSnpzsM0nDOhE40FTeNWOJ/YmySGV6tG6T8+hjz6vxbknHJr6A==", + "type": "package", + "path": "microsoft.csharp/4.0.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "ThirdPartyNotices.txt", + "dotnet_library_license.txt", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net45/_._", + "lib/netcore50/Microsoft.CSharp.dll", + "lib/netstandard1.3/Microsoft.CSharp.dll", + "lib/portable-net45+win8+wp8+wpa81/_._", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "microsoft.csharp.4.0.1.nupkg.sha512", + "microsoft.csharp.nuspec", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/net45/_._", + "ref/netcore50/Microsoft.CSharp.dll", + "ref/netcore50/Microsoft.CSharp.xml", + "ref/netcore50/de/Microsoft.CSharp.xml", + "ref/netcore50/es/Microsoft.CSharp.xml", + "ref/netcore50/fr/Microsoft.CSharp.xml", + "ref/netcore50/it/Microsoft.CSharp.xml", + "ref/netcore50/ja/Microsoft.CSharp.xml", + "ref/netcore50/ko/Microsoft.CSharp.xml", + "ref/netcore50/ru/Microsoft.CSharp.xml", + "ref/netcore50/zh-hans/Microsoft.CSharp.xml", + "ref/netcore50/zh-hant/Microsoft.CSharp.xml", + "ref/netstandard1.0/Microsoft.CSharp.dll", + "ref/netstandard1.0/Microsoft.CSharp.xml", + "ref/netstandard1.0/de/Microsoft.CSharp.xml", + "ref/netstandard1.0/es/Microsoft.CSharp.xml", + "ref/netstandard1.0/fr/Microsoft.CSharp.xml", + "ref/netstandard1.0/it/Microsoft.CSharp.xml", + "ref/netstandard1.0/ja/Microsoft.CSharp.xml", + "ref/netstandard1.0/ko/Microsoft.CSharp.xml", + "ref/netstandard1.0/ru/Microsoft.CSharp.xml", + "ref/netstandard1.0/zh-hans/Microsoft.CSharp.xml", + "ref/netstandard1.0/zh-hant/Microsoft.CSharp.xml", + "ref/portable-net45+win8+wp8+wpa81/_._", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._" + ] + }, + "Microsoft.NETCore.Platforms/3.1.3": { + "sha512": "EHOcrXuq/ELOANMUzrpHmGIRlu7tVYyvGYsYUKLrTNMeQMTmdN97S6PP9W/NLD1WjqljGO+tapTS3XuLPlJ4QA==", + "type": "package", + "path": "microsoft.netcore.platforms/3.1.3", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/netstandard1.0/_._", + "microsoft.netcore.platforms.3.1.3.nupkg.sha512", + "microsoft.netcore.platforms.nuspec", + "runtime.json", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "Microsoft.Win32.SystemEvents/4.7.0": { + "sha512": "mtVirZr++rq+XCDITMUdnETD59XoeMxSpLRIII7JRI6Yj0LEDiO1pPn0ktlnIj12Ix8bfvQqQDMMIF9wC98oCA==", + "type": "package", + "path": "microsoft.win32.systemevents/4.7.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/net461/Microsoft.Win32.SystemEvents.dll", + "lib/net461/Microsoft.Win32.SystemEvents.xml", + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll", + "lib/netstandard2.0/Microsoft.Win32.SystemEvents.xml", + "microsoft.win32.systemevents.4.7.0.nupkg.sha512", + "microsoft.win32.systemevents.nuspec", + "ref/net461/Microsoft.Win32.SystemEvents.dll", + "ref/net461/Microsoft.Win32.SystemEvents.xml", + "ref/net472/Microsoft.Win32.SystemEvents.dll", + "ref/net472/Microsoft.Win32.SystemEvents.xml", + "ref/netstandard2.0/Microsoft.Win32.SystemEvents.dll", + "ref/netstandard2.0/Microsoft.Win32.SystemEvents.xml", + "runtimes/win/lib/netcoreapp2.0/Microsoft.Win32.SystemEvents.dll", + "runtimes/win/lib/netcoreapp2.0/Microsoft.Win32.SystemEvents.xml", + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.dll", + "runtimes/win/lib/netcoreapp3.0/Microsoft.Win32.SystemEvents.xml", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "Newtonsoft.Json/13.0.3": { + "sha512": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==", + "type": "package", + "path": "newtonsoft.json/13.0.3", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.md", + "README.md", + "lib/net20/Newtonsoft.Json.dll", + "lib/net20/Newtonsoft.Json.xml", + "lib/net35/Newtonsoft.Json.dll", + "lib/net35/Newtonsoft.Json.xml", + "lib/net40/Newtonsoft.Json.dll", + "lib/net40/Newtonsoft.Json.xml", + "lib/net45/Newtonsoft.Json.dll", + "lib/net45/Newtonsoft.Json.xml", + "lib/net6.0/Newtonsoft.Json.dll", + "lib/net6.0/Newtonsoft.Json.xml", + "lib/netstandard1.0/Newtonsoft.Json.dll", + "lib/netstandard1.0/Newtonsoft.Json.xml", + "lib/netstandard1.3/Newtonsoft.Json.dll", + "lib/netstandard1.3/Newtonsoft.Json.xml", + "lib/netstandard2.0/Newtonsoft.Json.dll", + "lib/netstandard2.0/Newtonsoft.Json.xml", + "newtonsoft.json.13.0.3.nupkg.sha512", + "newtonsoft.json.nuspec", + "packageIcon.png" + ] + }, + "NuGet.Common/6.9.1": { + "sha512": "FbuWZBjQ1NJXBDqCwSddN2yvw3Plq3sTCIh0nc66Hu8jrNr+BOaxlKZv78jvJ+pSy8BvurYOdF9sl9KoORjrtg==", + "type": "package", + "path": "nuget.common/6.9.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "icon.png", + "lib/net472/NuGet.Common.dll", + "lib/netstandard2.0/NuGet.Common.dll", + "nuget.common.6.9.1.nupkg.sha512", + "nuget.common.nuspec" + ] + }, + "NuGet.Configuration/6.9.1": { + "sha512": "GM06pcUzWdNsizeGciqCjAhryfI1F/rQPETLDF+8pDRgzVpA+wKAR01/4aFU+IXzugnQ9LqOb5YyCRuR1OVZiQ==", + "type": "package", + "path": "nuget.configuration/6.9.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "icon.png", + "lib/net472/NuGet.Configuration.dll", + "lib/netstandard2.0/NuGet.Configuration.dll", + "nuget.configuration.6.9.1.nupkg.sha512", + "nuget.configuration.nuspec" + ] + }, + "NuGet.Frameworks/6.9.1": { + "sha512": "DaKh3lenPUvzGccPkbI97BIvA27z+/UsL3ankfoZlX/4vBVDK5N1sheFTQ+GuJf+IgSzsJz/A21SPUpQLHwUtA==", + "type": "package", + "path": "nuget.frameworks/6.9.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "icon.png", + "lib/net472/NuGet.Frameworks.dll", + "lib/netstandard2.0/NuGet.Frameworks.dll", + "nuget.frameworks.6.9.1.nupkg.sha512", + "nuget.frameworks.nuspec" + ] + }, + "NuGet.Packaging/6.9.1": { + "sha512": "6FyasOxKInCELJ+pGy8f17ub9st6ofFeNcBNTo7CRiPJlxyhJfKGKNpfe3HHYwvnZhc3Vdfr0kSR+f1BVGDuTA==", + "type": "package", + "path": "nuget.packaging/6.9.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "icon.png", + "lib/net472/NuGet.Packaging.dll", + "lib/net5.0/NuGet.Packaging.dll", + "lib/netstandard2.0/NuGet.Packaging.dll", + "nuget.packaging.6.9.1.nupkg.sha512", + "nuget.packaging.nuspec" + ] + }, + "NuGet.Protocol/6.9.1": { + "sha512": "h3bdjqUY4jvwM02D/7QM4FR8x/bbf4Pyjrc1Etw7an2OrWKPUSx0vJPdapKzioxIw7GXl/aVUM/DCeIc3S9brA==", + "type": "package", + "path": "nuget.protocol/6.9.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "icon.png", + "lib/net472/NuGet.Protocol.dll", + "lib/net5.0/NuGet.Protocol.dll", + "lib/netstandard2.0/NuGet.Protocol.dll", + "nuget.protocol.6.9.1.nupkg.sha512", + "nuget.protocol.nuspec" + ] + }, + "NuGet.Resolver/6.9.1": { + "sha512": "Y/N0ACMIC3zAECrF94mjeS+kLV444UALGcCmixSWNs48sPorV36VISpdmq+qjCajpUFXIriFFCWOQ4hqGGR+4g==", + "type": "package", + "path": "nuget.resolver/6.9.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "icon.png", + "lib/net472/NuGet.Resolver.dll", + "lib/net5.0/NuGet.Resolver.dll", + "lib/netstandard2.0/NuGet.Resolver.dll", + "nuget.resolver.6.9.1.nupkg.sha512", + "nuget.resolver.nuspec" + ] + }, + "NuGet.Versioning/6.9.1": { + "sha512": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ==", + "type": "package", + "path": "nuget.versioning/6.9.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "icon.png", + "lib/net472/NuGet.Versioning.dll", + "lib/netstandard2.0/NuGet.Versioning.dll", + "nuget.versioning.6.9.1.nupkg.sha512", + "nuget.versioning.nuspec" + ] + }, + "Serilog/3.1.0": { + "sha512": "UPJGG8Hz12obhtAELHb0q83j0YpO1vGCypUbH0P4wMZnrpcqmrSLhHkZ/4Ojc+iNVpLwZ/wPBVC3lwSzzoZ/MQ==", + "type": "package", + "path": "serilog/3.1.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "icon.png", + "lib/net462/Serilog.dll", + "lib/net462/Serilog.xml", + "lib/net471/Serilog.dll", + "lib/net471/Serilog.xml", + "lib/net5.0/Serilog.dll", + "lib/net5.0/Serilog.xml", + "lib/net6.0/Serilog.dll", + "lib/net6.0/Serilog.xml", + "lib/net7.0/Serilog.dll", + "lib/net7.0/Serilog.xml", + "lib/netstandard2.0/Serilog.dll", + "lib/netstandard2.0/Serilog.xml", + "lib/netstandard2.1/Serilog.dll", + "lib/netstandard2.1/Serilog.xml", + "serilog.3.1.0.nupkg.sha512", + "serilog.nuspec" + ] + }, + "System.Buffers/4.5.1": { + "sha512": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==", + "type": "package", + "path": "system.buffers/4.5.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/net461/System.Buffers.dll", + "lib/net461/System.Buffers.xml", + "lib/netcoreapp2.0/_._", + "lib/netstandard1.1/System.Buffers.dll", + "lib/netstandard1.1/System.Buffers.xml", + "lib/netstandard2.0/System.Buffers.dll", + "lib/netstandard2.0/System.Buffers.xml", + "lib/uap10.0.16299/_._", + "ref/net45/System.Buffers.dll", + "ref/net45/System.Buffers.xml", + "ref/netcoreapp2.0/_._", + "ref/netstandard1.1/System.Buffers.dll", + "ref/netstandard1.1/System.Buffers.xml", + "ref/netstandard2.0/System.Buffers.dll", + "ref/netstandard2.0/System.Buffers.xml", + "ref/uap10.0.16299/_._", + "system.buffers.4.5.1.nupkg.sha512", + "system.buffers.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "System.ClientModel/1.0.0": { + "sha512": "I3CVkvxeqFYjIVEP59DnjbeoGNfo/+SZrCLpRz2v/g0gpCHaEMPtWSY0s9k/7jR1rAsLNg2z2u1JRB76tPjnIw==", + "type": "package", + "path": "system.clientmodel/1.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "CHANGELOG.md", + "DotNetPackageIcon.png", + "README.md", + "lib/net6.0/System.ClientModel.dll", + "lib/net6.0/System.ClientModel.xml", + "lib/netstandard2.0/System.ClientModel.dll", + "lib/netstandard2.0/System.ClientModel.xml", + "system.clientmodel.1.0.0.nupkg.sha512", + "system.clientmodel.nuspec" + ] + }, + "System.Diagnostics.DiagnosticSource/7.0.2": { + "sha512": "hYr3I9N9811e0Bjf2WNwAGGyTuAFbbTgX1RPLt/3Wbm68x3IGcX5Cl75CMmgT6WlNwLQ2tCCWfqYPpypjaf2xA==", + "type": "package", + "path": "system.diagnostics.diagnosticsource/7.0.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/System.Diagnostics.DiagnosticSource.targets", + "buildTransitive/net462/_._", + "buildTransitive/net6.0/_._", + "buildTransitive/netcoreapp2.0/System.Diagnostics.DiagnosticSource.targets", + "lib/net462/System.Diagnostics.DiagnosticSource.dll", + "lib/net462/System.Diagnostics.DiagnosticSource.xml", + "lib/net6.0/System.Diagnostics.DiagnosticSource.dll", + "lib/net6.0/System.Diagnostics.DiagnosticSource.xml", + "lib/net7.0/System.Diagnostics.DiagnosticSource.dll", + "lib/net7.0/System.Diagnostics.DiagnosticSource.xml", + "lib/netstandard2.0/System.Diagnostics.DiagnosticSource.dll", + "lib/netstandard2.0/System.Diagnostics.DiagnosticSource.xml", + "system.diagnostics.diagnosticsource.7.0.2.nupkg.sha512", + "system.diagnostics.diagnosticsource.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "System.Drawing.Common/4.7.1": { + "sha512": "fnb3qshElEe2lgIMBpc+Pww4xAjqa23yAAqTrtS9RX8uvnjoMJPTDbAVik2jYaOnFs3QpFMujYmAq0VS9101zA==", + "type": "package", + "path": "system.drawing.common/4.7.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net461/System.Drawing.Common.dll", + "lib/netstandard2.0/System.Drawing.Common.dll", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/net461/System.Drawing.Common.dll", + "ref/netcoreapp3.0/System.Drawing.Common.dll", + "ref/netcoreapp3.0/System.Drawing.Common.xml", + "ref/netstandard2.0/System.Drawing.Common.dll", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._", + "runtimes/unix/lib/netcoreapp2.0/System.Drawing.Common.dll", + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.dll", + "runtimes/unix/lib/netcoreapp3.0/System.Drawing.Common.xml", + "runtimes/win/lib/netcoreapp2.0/System.Drawing.Common.dll", + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.dll", + "runtimes/win/lib/netcoreapp3.0/System.Drawing.Common.xml", + "system.drawing.common.4.7.1.nupkg.sha512", + "system.drawing.common.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "System.Formats.Asn1/6.0.0": { + "sha512": "T6fD00dQ3NTbPDy31m4eQUwKW84s03z0N2C8HpOklyeaDgaJPa/TexP4/SkORMSOwc7WhKifnA6Ya33AkzmafA==", + "type": "package", + "path": "system.formats.asn1/6.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/netcoreapp2.0/System.Formats.Asn1.targets", + "buildTransitive/netcoreapp3.1/_._", + "lib/net461/System.Formats.Asn1.dll", + "lib/net461/System.Formats.Asn1.xml", + "lib/net6.0/System.Formats.Asn1.dll", + "lib/net6.0/System.Formats.Asn1.xml", + "lib/netstandard2.0/System.Formats.Asn1.dll", + "lib/netstandard2.0/System.Formats.Asn1.xml", + "system.formats.asn1.6.0.0.nupkg.sha512", + "system.formats.asn1.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "System.Memory/4.5.5": { + "sha512": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "type": "package", + "path": "system.memory/4.5.5", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/net461/System.Memory.dll", + "lib/net461/System.Memory.xml", + "lib/netcoreapp2.1/_._", + "lib/netstandard1.1/System.Memory.dll", + "lib/netstandard1.1/System.Memory.xml", + "lib/netstandard2.0/System.Memory.dll", + "lib/netstandard2.0/System.Memory.xml", + "ref/netcoreapp2.1/_._", + "system.memory.4.5.5.nupkg.sha512", + "system.memory.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "System.Memory.Data/1.0.2": { + "sha512": "JGkzeqgBsiZwKJZ1IxPNsDFZDhUvuEdX8L8BDC8N3KOj+6zMcNU28CNN59TpZE/VJYy9cP+5M+sbxtWJx3/xtw==", + "type": "package", + "path": "system.memory.data/1.0.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "CHANGELOG.md", + "DotNetPackageIcon.png", + "README.md", + "lib/net461/System.Memory.Data.dll", + "lib/net461/System.Memory.Data.xml", + "lib/netstandard2.0/System.Memory.Data.dll", + "lib/netstandard2.0/System.Memory.Data.xml", + "system.memory.data.1.0.2.nupkg.sha512", + "system.memory.data.nuspec" + ] + }, + "System.Numerics.Vectors/4.5.0": { + "sha512": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==", + "type": "package", + "path": "system.numerics.vectors/4.5.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net46/System.Numerics.Vectors.dll", + "lib/net46/System.Numerics.Vectors.xml", + "lib/netcoreapp2.0/_._", + "lib/netstandard1.0/System.Numerics.Vectors.dll", + "lib/netstandard1.0/System.Numerics.Vectors.xml", + "lib/netstandard2.0/System.Numerics.Vectors.dll", + "lib/netstandard2.0/System.Numerics.Vectors.xml", + "lib/portable-net45+win8+wp8+wpa81/System.Numerics.Vectors.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Numerics.Vectors.xml", + "lib/uap10.0.16299/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/net45/System.Numerics.Vectors.dll", + "ref/net45/System.Numerics.Vectors.xml", + "ref/net46/System.Numerics.Vectors.dll", + "ref/net46/System.Numerics.Vectors.xml", + "ref/netcoreapp2.0/_._", + "ref/netstandard1.0/System.Numerics.Vectors.dll", + "ref/netstandard1.0/System.Numerics.Vectors.xml", + "ref/netstandard2.0/System.Numerics.Vectors.dll", + "ref/netstandard2.0/System.Numerics.Vectors.xml", + "ref/uap10.0.16299/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._", + "system.numerics.vectors.4.5.0.nupkg.sha512", + "system.numerics.vectors.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": { + "sha512": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", + "type": "package", + "path": "system.runtime.compilerservices.unsafe/6.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/netcoreapp2.0/System.Runtime.CompilerServices.Unsafe.targets", + "buildTransitive/netcoreapp3.1/_._", + "lib/net461/System.Runtime.CompilerServices.Unsafe.dll", + "lib/net461/System.Runtime.CompilerServices.Unsafe.xml", + "lib/net6.0/System.Runtime.CompilerServices.Unsafe.dll", + "lib/net6.0/System.Runtime.CompilerServices.Unsafe.xml", + "lib/netcoreapp3.1/System.Runtime.CompilerServices.Unsafe.dll", + "lib/netcoreapp3.1/System.Runtime.CompilerServices.Unsafe.xml", + "lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll", + "lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.xml", + "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512", + "system.runtime.compilerservices.unsafe.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "System.Security.Cryptography.Pkcs/6.0.4": { + "sha512": "LGbXi1oUJ9QgCNGXRO9ndzBL/GZgANcsURpMhNR8uO+rca47SZmciS3RSQUvlQRwK3QHZSHNOXzoMUASKA+Anw==", + "type": "package", + "path": "system.security.cryptography.pkcs/6.0.4", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/netcoreapp2.0/System.Security.Cryptography.Pkcs.targets", + "buildTransitive/netcoreapp3.1/_._", + "lib/net461/System.Security.Cryptography.Pkcs.dll", + "lib/net461/System.Security.Cryptography.Pkcs.xml", + "lib/net6.0/System.Security.Cryptography.Pkcs.dll", + "lib/net6.0/System.Security.Cryptography.Pkcs.xml", + "lib/netcoreapp3.1/System.Security.Cryptography.Pkcs.dll", + "lib/netcoreapp3.1/System.Security.Cryptography.Pkcs.xml", + "lib/netstandard2.0/System.Security.Cryptography.Pkcs.dll", + "lib/netstandard2.0/System.Security.Cryptography.Pkcs.xml", + "lib/netstandard2.1/System.Security.Cryptography.Pkcs.dll", + "lib/netstandard2.1/System.Security.Cryptography.Pkcs.xml", + "runtimes/win/lib/net461/System.Security.Cryptography.Pkcs.dll", + "runtimes/win/lib/net461/System.Security.Cryptography.Pkcs.xml", + "runtimes/win/lib/net6.0/System.Security.Cryptography.Pkcs.dll", + "runtimes/win/lib/net6.0/System.Security.Cryptography.Pkcs.xml", + "runtimes/win/lib/netcoreapp3.1/System.Security.Cryptography.Pkcs.dll", + "runtimes/win/lib/netcoreapp3.1/System.Security.Cryptography.Pkcs.xml", + "runtimes/win/lib/netstandard2.0/System.Security.Cryptography.Pkcs.dll", + "runtimes/win/lib/netstandard2.0/System.Security.Cryptography.Pkcs.xml", + "runtimes/win/lib/netstandard2.1/System.Security.Cryptography.Pkcs.dll", + "runtimes/win/lib/netstandard2.1/System.Security.Cryptography.Pkcs.xml", + "system.security.cryptography.pkcs.6.0.4.nupkg.sha512", + "system.security.cryptography.pkcs.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "System.Security.Cryptography.ProtectedData/4.4.0": { + "sha512": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog==", + "type": "package", + "path": "system.security.cryptography.protecteddata/4.4.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net46/System.Security.Cryptography.ProtectedData.dll", + "lib/net461/System.Security.Cryptography.ProtectedData.dll", + "lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll", + "lib/netstandard2.0/System.Security.Cryptography.ProtectedData.dll", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/net46/System.Security.Cryptography.ProtectedData.dll", + "ref/net461/System.Security.Cryptography.ProtectedData.dll", + "ref/net461/System.Security.Cryptography.ProtectedData.xml", + "ref/netstandard1.3/System.Security.Cryptography.ProtectedData.dll", + "ref/netstandard2.0/System.Security.Cryptography.ProtectedData.dll", + "ref/netstandard2.0/System.Security.Cryptography.ProtectedData.xml", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._", + "runtimes/win/lib/net46/System.Security.Cryptography.ProtectedData.dll", + "runtimes/win/lib/net461/System.Security.Cryptography.ProtectedData.dll", + "runtimes/win/lib/netstandard1.3/System.Security.Cryptography.ProtectedData.dll", + "runtimes/win/lib/netstandard2.0/System.Security.Cryptography.ProtectedData.dll", + "system.security.cryptography.protecteddata.4.4.0.nupkg.sha512", + "system.security.cryptography.protecteddata.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "System.Text.Encodings.Web/8.0.0": { + "sha512": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", + "type": "package", + "path": "system.text.encodings.web/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/System.Text.Encodings.Web.targets", + "buildTransitive/net462/_._", + "buildTransitive/net6.0/_._", + "buildTransitive/netcoreapp2.0/System.Text.Encodings.Web.targets", + "lib/net462/System.Text.Encodings.Web.dll", + "lib/net462/System.Text.Encodings.Web.xml", + "lib/net6.0/System.Text.Encodings.Web.dll", + "lib/net6.0/System.Text.Encodings.Web.xml", + "lib/net7.0/System.Text.Encodings.Web.dll", + "lib/net7.0/System.Text.Encodings.Web.xml", + "lib/net8.0/System.Text.Encodings.Web.dll", + "lib/net8.0/System.Text.Encodings.Web.xml", + "lib/netstandard2.0/System.Text.Encodings.Web.dll", + "lib/netstandard2.0/System.Text.Encodings.Web.xml", + "runtimes/browser/lib/net6.0/System.Text.Encodings.Web.dll", + "runtimes/browser/lib/net6.0/System.Text.Encodings.Web.xml", + "runtimes/browser/lib/net7.0/System.Text.Encodings.Web.dll", + "runtimes/browser/lib/net7.0/System.Text.Encodings.Web.xml", + "runtimes/browser/lib/net8.0/System.Text.Encodings.Web.dll", + "runtimes/browser/lib/net8.0/System.Text.Encodings.Web.xml", + "system.text.encodings.web.8.0.0.nupkg.sha512", + "system.text.encodings.web.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "System.Text.Json/8.0.0": { + "sha512": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==", + "type": "package", + "path": "system.text.json/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "analyzers/dotnet/roslyn3.11/cs/System.Text.Json.SourceGeneration.dll", + "analyzers/dotnet/roslyn3.11/cs/cs/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/de/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/es/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/fr/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/it/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/ja/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/ko/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/pl/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/pt-BR/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/ru/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/tr/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/zh-Hans/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/zh-Hant/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/System.Text.Json.SourceGeneration.dll", + "analyzers/dotnet/roslyn4.0/cs/cs/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/de/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/es/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/fr/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/it/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/ja/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/ko/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/pl/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/pt-BR/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/ru/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/tr/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/zh-Hans/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/zh-Hant/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/System.Text.Json.SourceGeneration.dll", + "analyzers/dotnet/roslyn4.4/cs/cs/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/de/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/es/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/fr/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/it/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/ja/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/ko/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/pl/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/pt-BR/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/ru/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/tr/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/zh-Hans/System.Text.Json.SourceGeneration.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/zh-Hant/System.Text.Json.SourceGeneration.resources.dll", + "buildTransitive/net461/System.Text.Json.targets", + "buildTransitive/net462/System.Text.Json.targets", + "buildTransitive/net6.0/System.Text.Json.targets", + "buildTransitive/netcoreapp2.0/System.Text.Json.targets", + "buildTransitive/netstandard2.0/System.Text.Json.targets", + "lib/net462/System.Text.Json.dll", + "lib/net462/System.Text.Json.xml", + "lib/net6.0/System.Text.Json.dll", + "lib/net6.0/System.Text.Json.xml", + "lib/net7.0/System.Text.Json.dll", + "lib/net7.0/System.Text.Json.xml", + "lib/net8.0/System.Text.Json.dll", + "lib/net8.0/System.Text.Json.xml", + "lib/netstandard2.0/System.Text.Json.dll", + "lib/netstandard2.0/System.Text.Json.xml", + "system.text.json.8.0.0.nupkg.sha512", + "system.text.json.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "System.Threading.Tasks.Extensions/4.5.4": { + "sha512": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "type": "package", + "path": "system.threading.tasks.extensions/4.5.4", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net461/System.Threading.Tasks.Extensions.dll", + "lib/net461/System.Threading.Tasks.Extensions.xml", + "lib/netcoreapp2.1/_._", + "lib/netstandard1.0/System.Threading.Tasks.Extensions.dll", + "lib/netstandard1.0/System.Threading.Tasks.Extensions.xml", + "lib/netstandard2.0/System.Threading.Tasks.Extensions.dll", + "lib/netstandard2.0/System.Threading.Tasks.Extensions.xml", + "lib/portable-net45+win8+wp8+wpa81/System.Threading.Tasks.Extensions.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Threading.Tasks.Extensions.xml", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/netcoreapp2.1/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._", + "system.threading.tasks.extensions.4.5.4.nupkg.sha512", + "system.threading.tasks.extensions.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "System.ValueTuple/4.5.0": { + "sha512": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==", + "type": "package", + "path": "system.valuetuple/4.5.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net461/System.ValueTuple.dll", + "lib/net461/System.ValueTuple.xml", + "lib/net47/System.ValueTuple.dll", + "lib/net47/System.ValueTuple.xml", + "lib/netcoreapp2.0/_._", + "lib/netstandard1.0/System.ValueTuple.dll", + "lib/netstandard1.0/System.ValueTuple.xml", + "lib/netstandard2.0/_._", + "lib/portable-net40+sl4+win8+wp8/System.ValueTuple.dll", + "lib/portable-net40+sl4+win8+wp8/System.ValueTuple.xml", + "lib/uap10.0.16299/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/net461/System.ValueTuple.dll", + "ref/net47/System.ValueTuple.dll", + "ref/netcoreapp2.0/_._", + "ref/netstandard2.0/_._", + "ref/portable-net40+sl4+win8+wp8/System.ValueTuple.dll", + "ref/uap10.0.16299/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._", + "system.valuetuple.4.5.0.nupkg.sha512", + "system.valuetuple.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "CustomProjectName/1.0.0": { + "type": "project", + "path": "../TestingProjectReferences/TestingProjectReferences.csproj", + "msbuildProject": "../TestingProjectReferences/TestingProjectReferences.csproj" + }, + "DotnetNuGetWhyPackage/1.0.0": { + "type": "project", + "path": "../DotnetNuGetWhyPackage/DotnetNuGetWhyPackage.csproj", + "msbuildProject": "../DotnetNuGetWhyPackage/DotnetNuGetWhyPackage.csproj" + } + }, + "projectFileDependencyGroups": { + ".NETFramework,Version=v4.7.2": [ + "Azure.Core >= 1.38.0", + "CustomProjectName >= 1.0.0", + "DotnetNuGetWhyPackage >= 1.0.0", + "System.Text.Json >= 8.0.0" + ], + "net6.0": [ + "CustomProjectName >= 1.0.0", + "DotnetNuGetWhyPackage >= 1.0.0", + "NuGet.Resolver >= 6.9.1", + "System.Text.Json >= 8.0.0" + ] + }, + "packageFolders": { + "C:\\Users\\advaytandon\\.nuget\\packages\\": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\DNW.Test.SampleProject1\\DNW.Test.SampleProject1.csproj", + "projectName": "DNW.Test.SampleProject1", + "projectPath": "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\DNW.Test.SampleProject1\\DNW.Test.SampleProject1.csproj", + "packagesPath": "C:\\Users\\advaytandon\\.nuget\\packages\\", + "outputPath": "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\DNW.Test.SampleProject1\\obj\\", + "projectStyle": "PackageReference", + "crossTargeting": true, + "configFilePaths": [ + "C:\\Users\\advaytandon\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net472", + "net6.0" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "C:\\Program Files\\dotnet\\library-packs": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net6.0": { + "targetAlias": "net6.0", + "projectReferences": { + "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\DotnetNuGetWhyPackage\\DotnetNuGetWhyPackage.csproj": { + "projectPath": "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\DotnetNuGetWhyPackage\\DotnetNuGetWhyPackage.csproj" + }, + "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\TestingProjectReferences\\TestingProjectReferences.csproj": { + "projectPath": "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\TestingProjectReferences\\TestingProjectReferences.csproj" + } + } + }, + "net472": { + "targetAlias": "net472", + "projectReferences": { + "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\DotnetNuGetWhyPackage\\DotnetNuGetWhyPackage.csproj": { + "projectPath": "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\DotnetNuGetWhyPackage\\DotnetNuGetWhyPackage.csproj" + }, + "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\TestingProjectReferences\\TestingProjectReferences.csproj": { + "projectPath": "C:\\Users\\advaytandon\\source\\packages\\DotnetNuGetWhyPackage\\TestingProjectReferences\\TestingProjectReferences.csproj" + } + } + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + } + }, + "frameworks": { + "net6.0": { + "targetAlias": "net6.0", + "dependencies": { + "NuGet.Resolver": { + "target": "Package", + "version": "[6.9.1, )" + }, + "System.Text.Json": { + "target": "Package", + "version": "[8.0.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.300\\RuntimeIdentifierGraph.json" + }, + "net472": { + "targetAlias": "net472", + "dependencies": { + "Azure.Core": { + "target": "Package", + "version": "[1.38.0, )" + }, + "System.Text.Json": { + "target": "Package", + "version": "[8.0.0, )" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.300\\RuntimeIdentifierGraph.json" + } + } + }, + "logs": [ + { + "code": "NU1605", + "level": "Error", + "message": "Warning As Error: Detected package downgrade: System.Text.Json from 8.0.3 to 8.0.0. Reference the package directly from the project to select a different version. \r\n DNW.Test.SampleProject1 -> DotnetNuGetWhyPackage -> System.Text.Json (>= 8.0.3) \r\n DNW.Test.SampleProject1 -> System.Text.Json (>= 8.0.0)", + "libraryId": "System.Text.Json", + "targetGraphs": [ + ".NETFramework,Version=v4.7.2", + "net6.0" + ] + } + ] +} \ No newline at end of file From 50394da5927ce4cc68d469d03e24fb2c6f959d9e Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 16 May 2024 15:59:51 -0700 Subject: [PATCH 44/53] removed unnecessary param --- .../WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs index 1663c4acb55..1232191f5fd 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs @@ -32,7 +32,7 @@ internal static class DependencyGraphFinder bool doesProjectHaveDependencyOnPackage = false; // get all top-level package and project references for the project, categorized by target framework alias - Dictionary> topLevelReferencesByFramework = GetTopLevelPackageAndProjectReferences(assetsFile, userInputFrameworks, assetsFile.PackageSpec.BaseDirectory); + Dictionary> topLevelReferencesByFramework = GetTopLevelPackageAndProjectReferences(assetsFile, userInputFrameworks); if (topLevelReferencesByFramework.Count > 0) { @@ -216,14 +216,12 @@ private static void AddToGraph( /// /// Assets file for the project. /// List of target framework aliases. - /// Root directory of the project. /// /// Dictionary mapping the project's target framework aliases to their respective top-level package and project references. /// private static Dictionary> GetTopLevelPackageAndProjectReferences( LockFile assetsFile, - List userInputFrameworks, - string projectDirectoryPath) + List userInputFrameworks) { var topLevelReferences = new Dictionary>(); @@ -242,7 +240,7 @@ private static Dictionary> GetTopLevelPackageAndProjectRefe foreach (var library in projectLibraries) { - projectLibraryPathToName.Add(Path.GetFullPath(library.Path, projectDirectoryPath), library.Name); + projectLibraryPathToName.Add(Path.GetFullPath(library.Path, assetsFile.PackageSpec.BaseDirectory), library.Name); } // get all top-level references for each target alias From 8f3db8934c5a8229692f1c8477e4b74d1fc86666 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 16 May 2024 16:09:31 -0700 Subject: [PATCH 45/53] trying to fix 'fully qualified path' error on linux and mac tests --- .../WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs index 1232191f5fd..60824d62ae0 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs @@ -237,10 +237,11 @@ private static Dictionary> GetTopLevelPackageAndProjectRefe // so we will store all project reference paths in a dictionary here var projectLibraries = assetsFile.Libraries.Where(l => l.Type == "project"); var projectLibraryPathToName = new Dictionary(projectLibraries.Count()); + var projectDirectoryPath = Path.GetFullPath(assetsFile.PackageSpec.BaseDirectory); foreach (var library in projectLibraries) { - projectLibraryPathToName.Add(Path.GetFullPath(library.Path, assetsFile.PackageSpec.BaseDirectory), library.Name); + projectLibraryPathToName.Add(Path.GetFullPath(library.Path, projectDirectoryPath), library.Name); } // get all top-level references for each target alias From d9c8890559d82116a0c7003b2e8d322ea65bcb81 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Tue, 28 May 2024 17:43:59 -0700 Subject: [PATCH 46/53] address feedback, try to fix unit tests path issue --- .../Commands/WhyCommand/DependencyNode.cs | 23 ++++- .../Commands/WhyCommand/WhyCommandRunner.cs | 83 ++++++++++++----- .../DependencyGraphFinder.cs | 58 ++++++------ .../DependencyGraphPrinter.cs | 6 +- .../Strings.Designer.cs | 18 ++-- .../NuGet.CommandLine.XPlat/Strings.resx | 7 +- .../Dotnet.Integration.Test/DotnetWhyTests.cs | 36 ++++++++ .../NuGet.XPlat.FuncTest/XPlatWhyTests.cs | 47 +++++++++- .../XPlatWhyUnitTests.cs | 92 +++++++++++++++++-- 9 files changed, 287 insertions(+), 83 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs index a3953de00af..05c73c4ce5c 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyNode.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Collections.Generic; using NuGet.Shared; @@ -19,9 +20,9 @@ internal class DependencyNode public DependencyNode(string id, string version) { - Id = id; - Version = version; - Children = new HashSet(); + Id = id ?? throw new ArgumentNullException(nameof(id)); + Version = version ?? throw new ArgumentNullException(nameof(version)); + Children = new HashSet(new DependencyNodeComparer()); } public override int GetHashCode() @@ -33,4 +34,20 @@ public override int GetHashCode() return hashCodeCombiner.CombinedHash; } } + + internal class DependencyNodeComparer : IEqualityComparer + { + public bool Equals(DependencyNode? x, DependencyNode? y) + { + if (x == null || y == null) + return false; + + return string.Equals(x.Id, y.Id, StringComparison.CurrentCultureIgnoreCase); + } + + public int GetHashCode(DependencyNode obj) + { + return obj.Id.GetHashCode(); + } + } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 64b8a19b803..a15c5bbd280 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -16,7 +16,6 @@ namespace NuGet.CommandLine.XPlat { internal static class WhyCommandRunner { - private const string ProjectName = "MSBuildProjectName"; private const string ProjectAssetsFile = "ProjectAssetsFile"; /// @@ -25,14 +24,10 @@ internal static class WhyCommandRunner /// CLI arguments for the 'why' command. public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) { - try + bool validArgumentsUsed = ValidatePathArgument(whyCommandArgs.Path, whyCommandArgs.Logger) + && ValidatePackageArgument(whyCommandArgs.Package, whyCommandArgs.Logger); + if (!validArgumentsUsed) { - ValidatePathArgument(whyCommandArgs.Path); - ValidatePackageArgument(whyCommandArgs.Package); - } - catch (ArgumentException ex) - { - whyCommandArgs.Logger.LogError(ex.Message); return ExitCodes.InvalidArguments; } @@ -49,7 +44,9 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) if (assetsFile != null) { - Dictionary?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphs( + ValidateFrameworksOptions(assetsFile, whyCommandArgs.Frameworks, whyCommandArgs.Logger); + + Dictionary?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphsForTarget( assetsFile, whyCommandArgs.Package, whyCommandArgs.Frameworks); @@ -59,7 +56,7 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) whyCommandArgs.Logger.LogMinimal( string.Format( Strings.WhyCommand_Message_DependencyGraphsFoundInProject, - project.GetPropertyValue(ProjectName), + assetsFile.PackageSpec.Name, targetPackage)); DependencyGraphPrinter.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); @@ -69,7 +66,7 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) whyCommandArgs.Logger.LogMinimal( string.Format( Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, - project.GetPropertyValue(ProjectName), + assetsFile.PackageSpec.Name, targetPackage)); } } @@ -80,36 +77,49 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) return ExitCodes.Success; } - private static void ValidatePathArgument(string path) + private static bool ValidatePathArgument(string path, ILoggerWithColor logger) { if (string.IsNullOrEmpty(path)) { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - "PROJECT|SOLUTION")); + logger.LogError( + string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + "PROJECT|SOLUTION")); + + return false; } if (!File.Exists(path) || (!path.EndsWith("proj", StringComparison.OrdinalIgnoreCase) && !path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_PathIsMissingOrInvalid, - path)); + logger.LogError( + string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_PathIsMissingOrInvalid, + path)); + + return false; } + + return true; } - private static void ValidatePackageArgument(string package) + private static bool ValidatePackageArgument(string package, ILoggerWithColor logger) { if (string.IsNullOrEmpty(package)) { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, - Strings.WhyCommand_Error_ArgumentCannotBeEmpty, - "PACKAGE")); + logger.LogError( + string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Error_ArgumentCannotBeEmpty, + "PACKAGE")); + + return false; } + + return true; } /// @@ -164,5 +174,28 @@ private static void ValidatePackageArgument(string package) return assetsFile; } + + /// + /// Validates that the input frameworks options have corresponding targets in the assets file. Outputs a warning message if a framework does not exist. + /// + /// + /// + /// + private static void ValidateFrameworksOptions(LockFile assetsFile, List inputFrameworks, ILoggerWithColor logger) + { + foreach (var frameworkAlias in inputFrameworks) + { + if (assetsFile.GetTarget(frameworkAlias, runtimeIdentifier: null) == null) + { + logger.LogWarning( + string.Format( + CultureInfo.CurrentCulture, + Strings.WhyCommand_Warning_AssetsFileDoesNotContainSpecifiedTarget, + assetsFile.Path, + assetsFile.PackageSpec.Name, + frameworkAlias)); + } + } + } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs index 60824d62ae0..9f6432861d4 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs @@ -23,7 +23,7 @@ internal static class DependencyGraphFinder /// Dictionary mapping target framework aliases to their respective dependency graphs. /// Returns null if the project does not have a dependency on the target package. /// - public static Dictionary?>? GetAllDependencyGraphs( + public static Dictionary?>? GetAllDependencyGraphsForTarget( LockFile assetsFile, string targetPackage, List userInputFrameworks) @@ -40,22 +40,19 @@ internal static class DependencyGraphFinder { LockFileTarget target = assetsFile.GetTarget(targetFrameworkAlias, runtimeIdentifier: null); - if (target != null) + // get all package libraries for the framework + IList packageLibraries = target.Libraries; + + // if the project has a dependency on the target package, get the dependency graph + if (packageLibraries.Any(l => l?.Name?.Equals(targetPackage, StringComparison.OrdinalIgnoreCase) == true)) + { + doesProjectHaveDependencyOnPackage = true; + dependencyGraphPerFramework.Add(targetFrameworkAlias, + GetDependencyGraphForTargetPerFramework(topLevelReferences, packageLibraries, targetPackage)); + } + else { - // get all package libraries for the framework - IList packageLibraries = target.Libraries; - - // if the project has a dependency on the target package, get the dependency graph - if (packageLibraries.Any(l => l.Name == targetPackage)) - { - doesProjectHaveDependencyOnPackage = true; - dependencyGraphPerFramework.Add(targetFrameworkAlias, - GetDependencyGraphPerFramework(topLevelReferences, packageLibraries, targetPackage)); - } - else - { - dependencyGraphPerFramework.Add(targetFrameworkAlias, null); - } + dependencyGraphPerFramework.Add(targetFrameworkAlias, null); } } } @@ -74,7 +71,7 @@ internal static class DependencyGraphFinder /// /// List of all top-level package nodes in the dependency graph. /// - private static List? GetDependencyGraphPerFramework( + private static List? GetDependencyGraphForTargetPerFramework( List topLevelReferences, IList packageLibraries, string targetPackage) @@ -82,16 +79,16 @@ internal static class DependencyGraphFinder List? dependencyGraph = null; // hashset tracking every package node that we've traversed - var visited = new HashSet(); + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); // dictionary tracking all package nodes that have been added to the graph, mapped to their DependencyNode objects - var dependencyNodes = new Dictionary(); + var dependencyNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); // dictionary mapping all packageIds to their resolved version Dictionary versions = GetAllResolvedVersions(packageLibraries); foreach (var topLevelReference in topLevelReferences) { // use depth-first search to find dependency paths from the top-level package to the target package - DependencyNode? topLevelNode = FindDependencyPath(topLevelReference, packageLibraries, visited, dependencyNodes, versions, targetPackage); + DependencyNode? topLevelNode = FindDependencyPathForTarget(topLevelReference, packageLibraries, visited, dependencyNodes, versions, targetPackage); if (topLevelNode != null) { @@ -115,7 +112,7 @@ internal static class DependencyGraphFinder /// /// The top-level package node in the dependency graph (if a path was found), or null (if no path was found). /// - private static DependencyNode? FindDependencyPath( + private static DependencyNode? FindDependencyPathForTarget( string topLevelPackage, IList packageLibraries, HashSet visited, @@ -132,7 +129,7 @@ internal static class DependencyGraphFinder var currentPackageId = currentPackageData.Id; // if we reach the target node, or if we've already traversed this node and found dependency paths, add it to the graph - if (currentPackageId == targetPackage + if (currentPackageId.Equals(targetPackage, StringComparison.OrdinalIgnoreCase) || dependencyNodes.ContainsKey(currentPackageId)) { AddToGraph(currentPackageData, dependencyNodes, versions); @@ -148,7 +145,7 @@ internal static class DependencyGraphFinder visited.Add(currentPackageId); // get all dependencies for the current package - var dependencies = packageLibraries?.FirstOrDefault(i => i.Name == currentPackageId)?.Dependencies; + var dependencies = packageLibraries?.FirstOrDefault(i => i?.Name?.Equals(currentPackageId, StringComparison.OrdinalIgnoreCase) == true)?.Dependencies; if (dependencies?.Count > 0) { @@ -200,12 +197,6 @@ private static void AddToGraph( if (i > 0) { var childNode = dependencyNodes[dependencyPath[i - 1]]; - - if (dependencyNodes[currentPackageId].Children.Any(p => p.Id == childNode.Id)) - { - continue; - } - dependencyNodes[currentPackageId].Children.Add(childNode); } } @@ -237,11 +228,14 @@ private static Dictionary> GetTopLevelPackageAndProjectRefe // so we will store all project reference paths in a dictionary here var projectLibraries = assetsFile.Libraries.Where(l => l.Type == "project"); var projectLibraryPathToName = new Dictionary(projectLibraries.Count()); - var projectDirectoryPath = Path.GetFullPath(assetsFile.PackageSpec.BaseDirectory); + var projectDirectoryPath = Path.GetDirectoryName(assetsFile.PackageSpec.FilePath); - foreach (var library in projectLibraries) + if (projectDirectoryPath != null) { - projectLibraryPathToName.Add(Path.GetFullPath(library.Path, projectDirectoryPath), library.Name); + foreach (var library in projectLibraries) + { + projectLibraryPathToName.Add(Path.GetFullPath(library.Path, projectDirectoryPath), library.Name); + } } // get all top-level references for each target alias diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs index a51301405e3..a55b80330b6 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs @@ -69,7 +69,7 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li // initialize the stack with all top-level nodes int counter = 0; - foreach (var node in topLevelNodes.OrderByDescending(c => c.Id, StringComparer.CurrentCulture)) + foreach (var node in topLevelNodes.OrderByDescending(c => c.Id, StringComparer.OrdinalIgnoreCase)) { stack.Push(new StackOutputData(node, prefix: " ", isLastChild: counter++ == 0)); } @@ -92,7 +92,7 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li } // print current node - if (current.Node.Id == targetPackage) + if (current.Node.Id.Equals(targetPackage, StringComparison.OrdinalIgnoreCase)) { logger.LogMinimal($"{currentPrefix}", Console.ForegroundColor); logger.LogMinimal($"{current.Node.Id} (v{current.Node.Version})\n", TargetPackageColor); @@ -106,7 +106,7 @@ private static void PrintDependencyGraphPerFramework(List frameworks, Li { // push all the node's children onto the stack counter = 0; - foreach (var child in current.Node.Children.OrderByDescending(c => c.Id, StringComparer.CurrentCulture)) + foreach (var child in current.Node.Children.OrderByDescending(c => c.Id, StringComparer.OrdinalIgnoreCase)) { stack.Push(new StackOutputData(child, childPrefix, isLastChild: counter++ == 0)); } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index b1657b04210..d000d0670b0 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -2232,15 +2232,6 @@ internal static string WhyCommand_Error_InvalidAssetsFile { } } - /// - /// Looks up a localized string similar to Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. For a list of accepted values, please visit https://learn.microsoft.com/en-us/nuget/reference/target-frameworks#supported-frameworks. - /// - internal static string WhyCommand_Error_InvalidFramework { - get { - return ResourceManager.GetString("WhyCommand_Error_InvalidFramework", resourceCulture); - } - } - /// /// Looks up a localized string similar to Unable to run 'dotnet nuget why'. Missing or invalid project/solution file '{0}'.. /// @@ -2303,5 +2294,14 @@ internal static string WhyCommand_PathArgument_Description { return ResourceManager.GetString("WhyCommand_PathArgument_Description", resourceCulture); } } + + /// + /// Looks up a localized string similar to The assets file {0} for project '{1}' does not contain a target for the specified input framework '{2}'.. + /// + internal static string WhyCommand_Warning_AssetsFileDoesNotContainSpecifiedTarget { + get { + return ResourceManager.GetString("WhyCommand_Warning_AssetsFileDoesNotContainSpecifiedTarget", resourceCulture); + } + } } } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index e51b90d75fc..f561a0f33bf 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -932,13 +932,14 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Unable to run 'dotnet nuget why'. Missing or invalid project/solution file '{0}'. {0} - Project/solution file path - - Failed to parse one of the given frameworks. Please make sure the given frameworks are valid. For a list of accepted values, please visit https://learn.microsoft.com/en-us/nuget/reference/target-frameworks#supported-frameworks - The assets file {0} is invalid. Please run restore for project '{1}' before running this command. {0} - Assets file path, {1} - Project name + + The assets file {0} for project '{1}' does not contain a target for the specified input framework '{2}'. + {0} - Assets file path, {1} - Project name, {2} - Framework + Project '{0}' does not have a dependency on '{1}'. {0} - Project name, {1} - Target package diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index cca1b5a329a..8985fac94f4 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -188,5 +188,41 @@ public void WhyCommand_EmptyPackageArgument_Fails() Assert.Equal(ExitCodes.InvalidArguments, result.ExitCode); Assert.Contains($"Required argument missing for command: 'why'.", result.Errors); } + + [Fact] + public async void WhyCommand_InvalidFrameworksOption_WarnsCorrectly() + { + // Arrange + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var inputFrameworksOption = "invalidFrameworkAlias"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1", projectFramework); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + string addPackageCommandArgs = $"add {project.ProjectPath} package {packageX.Id}"; + CommandRunnerResult addPackageResult = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, addPackageCommandArgs); + + string whyCommandArgs = $"nuget why {project.ProjectPath} {packageY.Id} -f {inputFrameworksOption} -f {projectFramework}"; + + // Act + CommandRunnerResult result = _testFixture.RunDotnetExpectSuccess(pathContext.SolutionRoot, whyCommandArgs); + + // Assert + Assert.Equal(ExitCodes.Success, result.ExitCode); + Assert.Contains($"warn : The assets file {project.AssetsFileOutputPath} for project '{ProjectName}' does not contain a target for the specified input framework '{inputFrameworksOption}'.", result.AllOutput); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", result.AllOutput); + } } } diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs index 7020ba830e4..40209f99eff 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/XPlatWhyTests.cs @@ -1,8 +1,6 @@ // 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 Moq; using NuGet.CommandLine.XPlat; using NuGet.Packaging; using NuGet.Test.Utility; @@ -204,5 +202,50 @@ public void WhyCommand_InvalidProject_Fails() Assert.Equal(ExitCodes.InvalidArguments, result); Assert.Contains($"Unable to run 'dotnet nuget why'. Missing or invalid project/solution file 'FakeProjectPath.csproj'.", errorOutput); } + + [Fact] + public async void WhyCommand_InvalidFrameworksOption_WarnsCorrectly() + { + // Arrange + var logger = new TestCommandOutputLogger(); + + var pathContext = new SimpleTestPathContext(); + var projectFramework = "net472"; + var inputFrameworksOption = "invalidFrameworkAlias"; + var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); + + var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); + var packageY = XPlatTestUtils.CreatePackage("PackageY", "1.0.1", projectFramework); + + packageX.Dependencies.Add(packageY); + + project.AddPackageToFramework(projectFramework, packageX); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageX, + packageY); + + var addPackageCommandArgs = XPlatTestUtils.GetPackageReferenceArgs(packageX.Id, packageX.Version, project); + var addPackageCommandRunner = new AddPackageReferenceCommandRunner(); + var addPackageResult = await addPackageCommandRunner.ExecuteCommand(addPackageCommandArgs, MsBuild); + + var whyCommandArgs = new WhyCommandArgs( + project.ProjectPath, + packageY.Id, + [inputFrameworksOption, projectFramework], + logger); + + // Act + var result = WhyCommandRunner.ExecuteCommand(whyCommandArgs); + + // Assert + var output = logger.ShowMessages(); + + Assert.Equal(ExitCodes.Success, result); + Assert.Contains($"The assets file {project.AssetsFileOutputPath} for project '{ProjectName}' does not contain a target for the specified input framework '{inputFrameworksOption}'.", output); + Assert.Contains($"Project '{ProjectName}' has the following dependency graph(s) for '{packageY.Id}'", output); + } } } diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs index 3cdf2442378..e6f683dd52c 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs @@ -5,6 +5,7 @@ using System.Linq; using NuGet.CommandLine.XPlat.WhyCommandUtility; using NuGet.ProjectModel; +using NuGet.Test.Utility; using Test.Utility; using Xunit; @@ -20,11 +21,16 @@ public void WhyCommand_DependencyGraphFinder_MultipleDependencyPathsForTargetPac var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + if (XunitAttributeUtility.CurrentPlatform == Platform.Linux || XunitAttributeUtility.CurrentPlatform == Platform.Darwin) + { + ConvertRelevantWindowsPathsToUnix(assetsFile); + } + string targetPackage = "System.Text.Json"; var frameworks = new List(); // Act - var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphsForTarget(assetsFile, targetPackage, frameworks); // Assert @@ -46,11 +52,16 @@ public void WhyCommand_DependencyGraphFinder_DependencyOnTargetProject_AllPathsF var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + if (XunitAttributeUtility.CurrentPlatform == Platform.Linux || XunitAttributeUtility.CurrentPlatform == Platform.Darwin) + { + ConvertRelevantWindowsPathsToUnix(assetsFile); + } + string targetPackage = "DotnetNuGetWhyPackage"; // project reference var frameworks = new List(); // Act - var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphsForTarget(assetsFile, targetPackage, frameworks); // Assert @@ -69,11 +80,16 @@ public void WhyCommand_DependencyGraphFinder_NoDependencyOnTargetPackage_Returns var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + if (XunitAttributeUtility.CurrentPlatform == Platform.Linux || XunitAttributeUtility.CurrentPlatform == Platform.Darwin) + { + ConvertRelevantWindowsPathsToUnix(assetsFile); + } + string targetPackage = "NotARealPackage"; var frameworks = new List(); // Act - var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphsForTarget(assetsFile, targetPackage, frameworks); // Assert @@ -89,11 +105,16 @@ public void WhyCommand_DependencyGraphFinder_DependencyOnTargetPackageForOnlyOne var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + if (XunitAttributeUtility.CurrentPlatform == Platform.Linux || XunitAttributeUtility.CurrentPlatform == Platform.Darwin) + { + ConvertRelevantWindowsPathsToUnix(assetsFile); + } + string targetPackage = "Azure.Core"; var frameworks = new List(); // Act - var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphsForTarget(assetsFile, targetPackage, frameworks); // Assert @@ -112,11 +133,16 @@ public void WhyCommand_DependencyGraphFinder_DependencyOnTargetPackage_Framework var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + if (XunitAttributeUtility.CurrentPlatform == Platform.Linux || XunitAttributeUtility.CurrentPlatform == Platform.Darwin) + { + ConvertRelevantWindowsPathsToUnix(assetsFile); + } + string targetPackage = "Azure.Core"; List frameworks = ["net472"]; // Act - var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphsForTarget(assetsFile, targetPackage, frameworks); // Assert @@ -132,16 +158,70 @@ public void WhyCommand_DependencyGraphFinder_DependencyOnTargetPackageForOnlyOne var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + if (XunitAttributeUtility.CurrentPlatform == Platform.Linux || XunitAttributeUtility.CurrentPlatform == Platform.Darwin) + { + ConvertRelevantWindowsPathsToUnix(assetsFile); + } + string targetPackage = "Azure.Core"; List frameworks = ["net6.0"]; // Act - var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphs(assetsFile, targetPackage, frameworks); + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphsForTarget(assetsFile, targetPackage, frameworks); // Assert // no paths found Assert.Null(dependencyGraphs); } + + [Fact] + public void WhyCommand_DependencyGraphFinder_DifferentCaseUsedForTargetPackageId_MatchesAreCaseInsensitive_AllPathsFound() + { + // Arrange + var lockFileFormat = new LockFileFormat(); + var lockFileContent = ProtocolUtility.GetResource("NuGet.CommandLine.Xplat.Tests.compiler.resources.DNW.Test.SampleProject1.project.assets.json", GetType()); + var assetsFile = lockFileFormat.Parse(lockFileContent, "In Memory"); + + if (XunitAttributeUtility.CurrentPlatform == Platform.Linux || XunitAttributeUtility.CurrentPlatform == Platform.Darwin) + { + ConvertRelevantWindowsPathsToUnix(assetsFile); + } + + string targetPackage = "system.text.jsoN"; + var frameworks = new List(); + + // Act + var dependencyGraphs = DependencyGraphFinder.GetAllDependencyGraphsForTarget(assetsFile, targetPackage, frameworks); + + // Assert + + // direct dependency on target package is found + Assert.Contains(dependencyGraphs["net472"], dep => (dep.Id == "System.Text.Json") && (dep.Version == "8.0.0")); + + // transitive dependency from a top-level package is found, with the correct resolved version + Assert.Contains(dependencyGraphs["net472"].First(dep => dep.Id == "Azure.Core").Children, dep => (dep.Id == "System.Text.Json") && (dep.Version == "8.0.0")); + + // transitive dependency from a top-level project reference is found, with the correct resolved version + Assert.Contains(dependencyGraphs["net472"].First(dep => dep.Id == "DotnetNuGetWhyPackage").Children, dep => (dep.Id == "System.Text.Json") && (dep.Version == "8.0.0")); + } + + private static void ConvertRelevantWindowsPathsToUnix(LockFile assetsFile) + { + assetsFile.PackageSpec.FilePath = ConvertWindowsPathToUnix(assetsFile.PackageSpec.FilePath); + + var projectLibraries = assetsFile.Libraries.Where(l => l.Type == "project"); + + foreach (var library in projectLibraries) + { + library.Path = ConvertWindowsPathToUnix(library.Path); + } + } + + private static string ConvertWindowsPathToUnix(string path) + { + char[] trimChars = [ 'C', ':' ]; + return path.TrimStart(trimChars).Replace("\\", "/"); + } } } From 6074b2506335f68814e3346ca028b29ee9d3f477 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Tue, 28 May 2024 18:01:16 -0700 Subject: [PATCH 47/53] fixed whitespace issue --- .../NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs index e6f683dd52c..6fd840278c6 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs @@ -220,7 +220,7 @@ private static void ConvertRelevantWindowsPathsToUnix(LockFile assetsFile) private static string ConvertWindowsPathToUnix(string path) { - char[] trimChars = [ 'C', ':' ]; + char[] trimChars = ['C', ':']; return path.TrimStart(trimChars).Replace("\\", "/"); } } From 7aceac8f9a9e72be442b8583d069a9dd642aee3f Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Tue, 28 May 2024 18:19:23 -0700 Subject: [PATCH 48/53] fixed unit test path issue for remaining file paths --- .../XPlatWhyUnitTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs index 6fd840278c6..fc7214f9f27 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs @@ -216,6 +216,17 @@ private static void ConvertRelevantWindowsPathsToUnix(LockFile assetsFile) { library.Path = ConvertWindowsPathToUnix(library.Path); } + + var packageSpecTargets = assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks; + + foreach (var target in packageSpecTargets) + { + var projectReferences = target.ProjectReferences; + foreach (var projectReference in projectReferences) + { + projectReference.ProjectPath = ConvertWindowsPathToUnix(projectReference.ProjectPath); + } + } } private static string ConvertWindowsPathToUnix(string path) From 8aff030c6c8acadd44f89c6799ce1e09f7c2f7f3 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Tue, 28 May 2024 19:52:08 -0700 Subject: [PATCH 49/53] trying to fix integration tests --- .../Dotnet.Integration.Test/DotnetWhyTests.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index 8985fac94f4..4b4a317782d 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using NuGet.CommandLine.XPlat; -using NuGet.Packaging; using NuGet.Test.Utility; using NuGet.XPlat.FuncTest; using Xunit; @@ -36,9 +35,8 @@ public async void WhyCommand_ProjectHasTransitiveDependency_DependencyPathExists project.AddPackageToFramework(projectFramework, packageX); - await SimpleTestPackageUtility.CreateFolderFeedV3Async( + await SimpleTestPackageUtility.CreatePackagesAsync( pathContext.PackageSource, - PackageSaveMode.Defaultv3, packageX, packageY); @@ -68,9 +66,8 @@ public async void WhyCommand_ProjectHasNoDependencyOnTargetPackage_PathDoesNotEx var packageZ = XPlatTestUtils.CreatePackage("PackageZ", "1.0.0", projectFramework); - await SimpleTestPackageUtility.CreateFolderFeedV3Async( + await SimpleTestPackageUtility.CreatePackagesAsync( pathContext.PackageSource, - PackageSaveMode.Defaultv3, packageX, packageZ); @@ -102,9 +99,8 @@ public async void WhyCommand_WithFrameworksOption_OptionParsedSuccessfully() project.AddPackageToFramework(projectFramework, packageX); - await SimpleTestPackageUtility.CreateFolderFeedV3Async( + await SimpleTestPackageUtility.CreatePackagesAsync( pathContext.PackageSource, - PackageSaveMode.Defaultv3, packageX, packageY); @@ -136,9 +132,8 @@ public async void WhyCommand_WithFrameworksOptionAlias_OptionParsedSuccessfully( project.AddPackageToFramework(projectFramework, packageX); - await SimpleTestPackageUtility.CreateFolderFeedV3Async( + await SimpleTestPackageUtility.CreatePackagesAsync( pathContext.PackageSource, - PackageSaveMode.Defaultv3, packageX, packageY); @@ -205,9 +200,8 @@ public async void WhyCommand_InvalidFrameworksOption_WarnsCorrectly() project.AddPackageToFramework(projectFramework, packageX); - await SimpleTestPackageUtility.CreateFolderFeedV3Async( + await SimpleTestPackageUtility.CreatePackagesAsync( pathContext.PackageSource, - PackageSaveMode.Defaultv3, packageX, packageY); From ee68859e147707dcdf257e4d9b908a8dac1a6cef Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 29 May 2024 15:29:30 -0700 Subject: [PATCH 50/53] removed WhyCommandUtility sub directory --- .../WhyCommand/{WhyCommandUtility => }/DependencyGraphFinder.cs | 2 +- .../{WhyCommandUtility => }/DependencyGraphPrinter.cs | 2 +- .../Commands/WhyCommand/WhyCommandRunner.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/{WhyCommandUtility => }/DependencyGraphFinder.cs (99%) rename src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/{WhyCommandUtility => }/DependencyGraphPrinter.cs (99%) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphFinder.cs similarity index 99% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphFinder.cs index 9f6432861d4..53953971a40 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphFinder.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphFinder.cs @@ -9,7 +9,7 @@ using System.Linq; using NuGet.ProjectModel; -namespace NuGet.CommandLine.XPlat.WhyCommandUtility +namespace NuGet.CommandLine.XPlat { internal static class DependencyGraphFinder { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphPrinter.cs similarity index 99% rename from src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs rename to src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphPrinter.cs index a55b80330b6..34c26ccedf6 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandUtility/DependencyGraphPrinter.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphPrinter.cs @@ -8,7 +8,7 @@ using System.Linq; using NuGet.Shared; -namespace NuGet.CommandLine.XPlat.WhyCommandUtility +namespace NuGet.CommandLine.XPlat { internal static class DependencyGraphPrinter { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index a15c5bbd280..1a63d57be85 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -9,7 +9,6 @@ using System.IO; using System.Linq; using Microsoft.Build.Evaluation; -using NuGet.CommandLine.XPlat.WhyCommandUtility; using NuGet.ProjectModel; namespace NuGet.CommandLine.XPlat From 118735de7a567645294cf9478da21568d4ceedf7 Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Wed, 29 May 2024 15:44:45 -0700 Subject: [PATCH 51/53] oops --- .../NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs index fc7214f9f27..944787748ef 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/XPlatWhyUnitTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using NuGet.CommandLine.XPlat.WhyCommandUtility; +using NuGet.CommandLine.XPlat; using NuGet.ProjectModel; using NuGet.Test.Utility; using Test.Utility; From 7b9a2e91e31a4568d958affcada7cb144a2a897f Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 30 May 2024 12:39:08 -0700 Subject: [PATCH 52/53] please work --- .../Dotnet.Integration.Test/DotnetWhyTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs index 4b4a317782d..3fb63aeed1b 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetWhyTests.cs @@ -25,7 +25,7 @@ public async void WhyCommand_ProjectHasTransitiveDependency_DependencyPathExists { // Arrange var pathContext = new SimpleTestPathContext(); - var projectFramework = "net472"; + var projectFramework = "net7.0"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); @@ -58,7 +58,7 @@ public async void WhyCommand_ProjectHasNoDependencyOnTargetPackage_PathDoesNotEx { // Arrange var pathContext = new SimpleTestPathContext(); - var projectFramework = "net472"; + var projectFramework = "net7.0"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); @@ -89,7 +89,7 @@ public async void WhyCommand_WithFrameworksOption_OptionParsedSuccessfully() { // Arrange var pathContext = new SimpleTestPathContext(); - var projectFramework = "net472"; + var projectFramework = "net7.0"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); @@ -122,7 +122,7 @@ public async void WhyCommand_WithFrameworksOptionAlias_OptionParsedSuccessfully( { // Arrange var pathContext = new SimpleTestPathContext(); - var projectFramework = "net472"; + var projectFramework = "net7.0"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); var packageX = XPlatTestUtils.CreatePackage("PackageX", "1.0.0", projectFramework); @@ -171,7 +171,7 @@ public void WhyCommand_EmptyPackageArgument_Fails() { // Arrange var pathContext = new SimpleTestPathContext(); - var projectFramework = "net472"; + var projectFramework = "net7.0"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); string whyCommandArgs = $"nuget why {project.ProjectPath}"; @@ -189,7 +189,7 @@ public async void WhyCommand_InvalidFrameworksOption_WarnsCorrectly() { // Arrange var pathContext = new SimpleTestPathContext(); - var projectFramework = "net472"; + var projectFramework = "net7.0"; var inputFrameworksOption = "invalidFrameworkAlias"; var project = XPlatTestUtils.CreateProject(ProjectName, pathContext, projectFramework); From 4a60b1374dd776605e678eea96fca99024f3dd3b Mon Sep 17 00:00:00 2001 From: Advay Tandon Date: Thu, 30 May 2024 19:19:57 -0700 Subject: [PATCH 53/53] skip non-sdk stype projects + some more null dereference handling --- .../WhyCommand/DependencyGraphFinder.cs | 8 +-- .../Commands/WhyCommand/WhyCommandRunner.cs | 59 +++++++++++-------- .../Strings.Designer.cs | 9 +++ .../NuGet.CommandLine.XPlat/Strings.resx | 3 + 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphFinder.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphFinder.cs index 53953971a40..ce0a93c4d69 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphFinder.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/DependencyGraphFinder.cs @@ -38,13 +38,13 @@ internal static class DependencyGraphFinder { foreach (var (targetFrameworkAlias, topLevelReferences) in topLevelReferencesByFramework) { - LockFileTarget target = assetsFile.GetTarget(targetFrameworkAlias, runtimeIdentifier: null); + LockFileTarget? target = assetsFile.GetTarget(targetFrameworkAlias, runtimeIdentifier: null); // get all package libraries for the framework - IList packageLibraries = target.Libraries; + IList? packageLibraries = target?.Libraries; // if the project has a dependency on the target package, get the dependency graph - if (packageLibraries.Any(l => l?.Name?.Equals(targetPackage, StringComparison.OrdinalIgnoreCase) == true)) + if (packageLibraries?.Any(l => l?.Name?.Equals(targetPackage, StringComparison.OrdinalIgnoreCase) == true) == true) { doesProjectHaveDependencyOnPackage = true; dependencyGraphPerFramework.Add(targetFrameworkAlias, @@ -272,7 +272,7 @@ private static Dictionary> GetTopLevelPackageAndProjectRefe /// All package libraries for a given framework. private static Dictionary GetAllResolvedVersions(IList packageLibraries) { - var versions = new Dictionary(); + var versions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var package in packageLibraries) { diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs index 1a63d57be85..7f8ebccc55f 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand/WhyCommandRunner.cs @@ -39,35 +39,48 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) foreach (var projectPath in projectPaths) { Project project = MSBuildAPIUtility.GetProject(projectPath); - LockFile? assetsFile = GetProjectAssetsFile(project, whyCommandArgs.Logger); - if (assetsFile != null) - { - ValidateFrameworksOptions(assetsFile, whyCommandArgs.Frameworks, whyCommandArgs.Logger); + string usingNetSdk = project.GetPropertyValue("UsingMicrosoftNETSdk"); - Dictionary?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphsForTarget( - assetsFile, - whyCommandArgs.Package, - whyCommandArgs.Frameworks); + if (!string.IsNullOrEmpty(usingNetSdk)) + { + LockFile? assetsFile = GetProjectAssetsFile(project, whyCommandArgs.Logger); - if (dependencyGraphPerFramework != null) + if (assetsFile != null) { - whyCommandArgs.Logger.LogMinimal( - string.Format( - Strings.WhyCommand_Message_DependencyGraphsFoundInProject, - assetsFile.PackageSpec.Name, - targetPackage)); - - DependencyGraphPrinter.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); + ValidateFrameworksOptions(assetsFile, whyCommandArgs.Frameworks, whyCommandArgs.Logger); + + Dictionary?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphsForTarget( + assetsFile, + whyCommandArgs.Package, + whyCommandArgs.Frameworks); + + if (dependencyGraphPerFramework != null) + { + whyCommandArgs.Logger.LogMinimal( + string.Format( + Strings.WhyCommand_Message_DependencyGraphsFoundInProject, + assetsFile.PackageSpec.Name, + targetPackage)); + + DependencyGraphPrinter.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); + } + else + { + whyCommandArgs.Logger.LogMinimal( + string.Format( + Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, + assetsFile.PackageSpec.Name, + targetPackage)); + } } - else - { - whyCommandArgs.Logger.LogMinimal( + } + else + { + whyCommandArgs.Logger.LogMinimal( string.Format( - Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, - assetsFile.PackageSpec.Name, - targetPackage)); - } + Strings.WhyCommand_Message_NonSDKStyleProjectsAreNotSupported, + project.GetPropertyValue("MSBuildProjectName"))); } ProjectCollection.GlobalProjectCollection.UnloadProject(project); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index d000d0670b0..23735887f82 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -2277,6 +2277,15 @@ internal static string WhyCommand_Message_NoDependencyGraphsFoundInProject { } } + /// + /// Looks up a localized string similar to Unable to run 'dotnet nuget why' for project '{0}'. This command only works on SDK-style projects.. + /// + internal static string WhyCommand_Message_NonSDKStyleProjectsAreNotSupported { + get { + return ResourceManager.GetString("WhyCommand_Message_NonSDKStyleProjectsAreNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to The package name to lookup in the dependency graph.. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index f561a0f33bf..236b2313371 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -932,6 +932,9 @@ Non-HTTPS access will be removed in a future version. Consider migrating to 'HTT Unable to run 'dotnet nuget why'. Missing or invalid project/solution file '{0}'. {0} - Project/solution file path + + Unable to run 'dotnet nuget why' for project '{0}'. This command only works on SDK-style projects. + The assets file {0} is invalid. Please run restore for project '{1}' before running this command. {0} - Assets file path, {1} - Project name