Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dotnet nuget why command #5761

Merged
merged 53 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
708f4e2
applied changes from pragnya17-dotnet-nuget-why
advay26 Mar 28, 2024
10245f8
cleaning up some old code
advay26 Mar 28, 2024
2098b79
wipping
advay26 Apr 3, 2024
3d428bc
fixed duplicate paths bug
advay26 Apr 3, 2024
db9a7ce
trees are beautiful
advay26 Apr 5, 2024
ef25ca2
rip my sleep schedule
advay26 Apr 5, 2024
9170c26
everything works now
advay26 Apr 5, 2024
b1b74f5
refactoring
advay26 Apr 6, 2024
067430a
cleaning up strings
advay26 Apr 6, 2024
08e95fc
fixed redundant traversal bug
advay26 Apr 7, 2024
ae50998
minor string stuff
advay26 Apr 8, 2024
3f153f9
remove (*) deduplication, fix project reference bug
advay26 Apr 10, 2024
8a750f1
wip: recursion -> stack
advay26 Apr 16, 2024
25804a8
recursion -> stack is working now
advay26 Apr 17, 2024
a44299b
wipping
advay26 Apr 18, 2024
073e9b5
removed TODOs to clean up
advay26 Apr 18, 2024
7ed373a
added basic test; failed to run basic test;
advay26 Apr 21, 2024
aaee328
test stubs - commented out
advay26 Apr 22, 2024
ebc4732
cleanup + removed debugging code
advay26 Apr 23, 2024
4a42f3e
Moved files to diff folder
advay26 Apr 23, 2024
d9a6482
experimenting with tests
advay26 Apr 24, 2024
904740e
started addressing feedback, but wow it's a lot of work
advay26 May 3, 2024
69bfb3b
adjusted tests to static class
advay26 May 3, 2024
cb0c043
wip
advay26 May 7, 2024
e55583a
nit
advay26 May 8, 2024
7891af4
temp: working on project refs
advay26 May 9, 2024
fa67ec9
still wipping....
advay26 May 9, 2024
961b7df
added tests, TODOs for targetAlias matching in GetResolvedVersions; r…
advay26 May 10, 2024
cf5771f
added new test files, will fill them out later
advay26 May 10, 2024
efaee79
fixed failing test
advay26 May 10, 2024
0bba3a1
addressin smore feedback
advay26 May 10, 2024
8ee8eba
added func tests + dotnet integration tests
advay26 May 13, 2024
31a6bf3
cleaning up for PR
advay26 May 13, 2024
5c3ad52
cleaning up
advay26 May 13, 2024
852edf0
register dotnet nuget/package * commands separately
advay26 May 15, 2024
923a1df
moved print methods out to new class
advay26 May 15, 2024
527d448
wip: addressing feedback, trialing new method for top level packages/…
advay26 May 15, 2024
506ee92
refactoring
advay26 May 16, 2024
1297105
reverted MSBuildAPIUtility changes
advay26 May 16, 2024
fec9de8
nit
advay26 May 16, 2024
b164b31
fixed tests (at least locally)
advay26 May 16, 2024
2a1c576
whitespace changes?
advay26 May 16, 2024
dccf34c
added unit tests, fixed func tests, + some small nits
advay26 May 16, 2024
50394da
removed unnecessary param
advay26 May 16, 2024
8f3db89
trying to fix 'fully qualified path' error on linux and mac tests
advay26 May 16, 2024
d9c8890
address feedback, try to fix unit tests path issue
advay26 May 29, 2024
6074b25
fixed whitespace issue
advay26 May 29, 2024
7aceac8
fixed unit test path issue for remaining file paths
advay26 May 29, 2024
8aff030
trying to fix integration tests
advay26 May 29, 2024
ee68859
removed WhyCommandUtility sub directory
advay26 May 29, 2024
118735d
oops
advay26 May 29, 2024
7b9a2e9
please work
advay26 May 30, 2024
4a60b13
skip non-sdk stype projects + some more null dereference handling
advay26 May 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// 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
{
internal static class DependencyGraphFinder
{
/// <summary>
/// Finds all dependency graphs for a given project.
/// </summary>
/// <param name="assetsFile">Assets file for the project.</param>
/// <param name="targetPackage">The package we want the dependency paths for.</param>
/// <param name="userInputFrameworks">List of target framework aliases.</param>
/// <returns>
/// Dictionary mapping target framework aliases to their respective dependency graphs.
/// Returns null if the project does not have a dependency on the target package.
/// </returns>
public static Dictionary<string, List<DependencyNode>?>? GetAllDependencyGraphsForTarget(
LockFile assetsFile,
string targetPackage,
List<string> userInputFrameworks)
{
var dependencyGraphPerFramework = new Dictionary<string, List<DependencyNode>?>(assetsFile.Targets.Count);
bool doesProjectHaveDependencyOnPackage = false;

// get all top-level package and project references for the project, categorized by target framework alias
Dictionary<string, List<string>> topLevelReferencesByFramework = GetTopLevelPackageAndProjectReferences(assetsFile, userInputFrameworks);

if (topLevelReferencesByFramework.Count > 0)
{
foreach (var (targetFrameworkAlias, topLevelReferences) in topLevelReferencesByFramework)
{
LockFileTarget? target = assetsFile.GetTarget(targetFrameworkAlias, runtimeIdentifier: null);

// get all package libraries for the framework
IList<LockFileTargetLibrary>? 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) == true)
{
doesProjectHaveDependencyOnPackage = true;
dependencyGraphPerFramework.Add(targetFrameworkAlias,
GetDependencyGraphForTargetPerFramework(topLevelReferences, packageLibraries, targetPackage));
}
else
{
dependencyGraphPerFramework.Add(targetFrameworkAlias, null);
}
}
}

return doesProjectHaveDependencyOnPackage
? dependencyGraphPerFramework
: null;
}

/// <summary>
/// Finds all dependency paths from the top-level packages to the target package for a given framework.
/// </summary>
/// <param name="topLevelReferences">All top-level package and project references for the framework.</param>
/// <param name="packageLibraries">All package libraries for the framework.</param>
/// <param name="targetPackage">The package we want the dependency paths for.</param>
/// <returns>
/// List of all top-level package nodes in the dependency graph.
/// </returns>
private static List<DependencyNode>? GetDependencyGraphForTargetPerFramework(
List<string> topLevelReferences,
IList<LockFileTargetLibrary> packageLibraries,
string targetPackage)
{
List<DependencyNode>? dependencyGraph = null;

// hashset tracking every package node that we've traversed
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// dictionary tracking all package nodes that have been added to the graph, mapped to their DependencyNode objects
var dependencyNodes = new Dictionary<string, DependencyNode>(StringComparer.OrdinalIgnoreCase);
// dictionary mapping all packageIds to their resolved version
Dictionary<string, string> 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 = FindDependencyPathForTarget(topLevelReference, packageLibraries, visited, dependencyNodes, versions, targetPackage);

if (topLevelNode != null)
{
dependencyGraph ??= [];
dependencyGraph.Add(topLevelNode);
}
}

return dependencyGraph;
}

/// <summary>
/// Traverses the dependency graph for a given top-level package, looking for a path to the target package.
/// </summary>
/// <param name="topLevelPackage">Top-level package.</param>
/// <param name="packageLibraries">All package libraries for a given framework.</param>
/// <param name="visited">HashSet tracking every package node that we've traversed.</param>
/// <param name="dependencyNodes">Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects.</param>
/// <param name="versions">Dictionary mapping packageIds to their resolved versions.</param>
/// <param name="targetPackage">The package we want the dependency paths for.</param>
/// <returns>
/// The top-level package node in the dependency graph (if a path was found), or null (if no path was found).
/// </returns>
private static DependencyNode? FindDependencyPathForTarget(
string topLevelPackage,
IList<LockFileTargetLibrary> packageLibraries,
HashSet<string> visited,
Dictionary<string, DependencyNode> dependencyNodes,
Dictionary<string, string> versions,
string targetPackage)
{
var stack = new Stack<StackDependencyData>();
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.Equals(targetPackage, StringComparison.OrdinalIgnoreCase)
|| 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?.Equals(currentPackageId, StringComparison.OrdinalIgnoreCase) == true)?.Dependencies;

if (dependencies?.Count > 0)
{
// push all the dependencies onto the stack
foreach (var dependency in dependencies)
{
stack.Push(new StackDependencyData(dependency.Id, currentPackageData));
}
}
}

return dependencyNodes.ContainsKey(topLevelPackage)
? dependencyNodes[topLevelPackage]
: null;
}

/// <summary>
/// Adds a dependency path to the graph, starting from the target package and traversing up to the top-level package.
/// </summary>
/// <param name="targetPackageData">Target node data. This stores parent references, so it can be used to construct the dependency graph
/// up to the top-level package.</param>
/// <param name="dependencyNodes">Dictionary tracking all packageIds that were added to the graph, mapped to their DependencyNode objects.</param>
/// <param name="versions">Dictionary mapping packageIds to their resolved versions.</param>
private static void AddToGraph(
StackDependencyData targetPackageData,
Dictionary<string, DependencyNode> dependencyNodes,
Dictionary<string, string> 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<string> { 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]];
dependencyNodes[currentPackageId].Children.Add(childNode);
}
}
}

/// <summary>
/// Get all top-level package and project references for the given project.
/// </summary>
/// <param name="assetsFile">Assets file for the project.</param>
/// <param name="userInputFrameworks">List of target framework aliases.</param>
/// <returns>
/// Dictionary mapping the project's target framework aliases to their respective top-level package and project references.
/// </returns>
private static Dictionary<string, List<string>> GetTopLevelPackageAndProjectReferences(
LockFile assetsFile,
List<string> userInputFrameworks)
{
var topLevelReferences = new Dictionary<string, List<string>>();

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<string, string>(projectLibraries.Count());
var projectDirectoryPath = Path.GetDirectoryName(assetsFile.PackageSpec.FilePath);

if (projectDirectoryPath != null)
{
foreach (var library in projectLibraries)
{
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
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;
}

/// <summary>
/// Adds all resolved versions of packages to a dictionary.
/// </summary>
/// <param name="packageLibraries">All package libraries for a given framework.</param>
private static Dictionary<string, string> GetAllResolvedVersions(IList<LockFileTargetLibrary> packageLibraries)
{
var versions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

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;
}
}
}
}
Loading
Loading