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

Dev/issue#50 #52

Merged
merged 5 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,6 @@ dotnet_diagnostic.CA1845.severity = silent

# CA1867: Use char overload => Not available in NetFramework
dotnet_diagnostic.CA1867.severity = silent

# CA1062: Validate arguments of public methods
dotnet_diagnostic.CA1062.severity = none
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</PropertyGroup>

<PropertyGroup Label="General Settings">
<LangVersion>11.0</LangVersion>
<LangVersion>12.0</LangVersion>
<DebugType>embedded</DebugType>
<DebugSymbols>true</DebugSymbols>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
Expand Down
8 changes: 8 additions & 0 deletions src/NuGetMonitor.Model/Models/PackageDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Packaging.Core;

namespace NuGetMonitor.Model.Models
{
public sealed record PackageDetails(PackageIdentity Identity, IReadOnlyCollection<PackageDependencyGroup> DependencyGroups, IReadOnlyCollection<NuGetFramework> SupportedFrameworks);
}
2 changes: 2 additions & 0 deletions src/NuGetMonitor.Model/Models/ProjectInTargetFramework.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public ProjectInTargetFramework(Project project, NuGetFramework targetFramework)

public string Name => Path.GetFileName(Project.FullPath);

public string NameAndFramework => $"{Name} ({TargetFramework})";

public IEnumerable<ProjectInTargetFramework> GetReferencedProjects(IEnumerable<ProjectInTargetFramework> allProjects)
{
return Project.GetItems("ProjectReference")
Expand Down
189 changes: 89 additions & 100 deletions src/NuGetMonitor.Model/Services/NuGetService.cs

Large diffs are not rendered by default.

83 changes: 7 additions & 76 deletions src/NuGetMonitor/Services/InfoBarService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ internal static class InfoBarService

private enum Actions
{
Manage
Manage,
ShowDependencyTree
}

public static void ShowTopLevelPackageIssues(IEnumerable<PackageReferenceInfo> topLevelPackages)
Expand Down Expand Up @@ -72,17 +73,12 @@ public static void ShowTransitivePackageIssues(ICollection<TransitiveDependencie
var textSpans = new[]
{
new InfoBarTextSpan(message),
new InfoBarHyperlink("Copy details", transitiveDependencies)
new InfoBarHyperlink("Open dependency tree", Actions.ShowDependencyTree)
};

ShowInfoBar(textSpans).FireAndForget();
}

private static void ShowInfoBar(string text, TimeSpan? timeOut = default)
{
ShowInfoBar(new[] { new InfoBarTextSpan(text) }, timeOut).FireAndForget();
}

private static async Task ShowInfoBar(IEnumerable<InfoBarTextSpan> textSpans, TimeSpan? timeOut = default)
{
var model = new InfoBarModel(textSpans, KnownMonikers.NuGet, isCloseButtonVisible: true);
Expand Down Expand Up @@ -118,8 +114,8 @@ private static void InfoBar_ActionItemClicked(object sender, InfoBarActionItemEv
OpenNuGetPackageManager();
break;

case ICollection<TransitiveDependencies> transitiveDependencies:
PrintDependencyTree(transitiveDependencies);
case Actions.ShowDependencyTree:
OpenDependencyTree();
break;
}

Expand All @@ -141,74 +137,9 @@ private static void OpenNuGetPackageManager()
}
}

private static void PrintDependencyTree(IEnumerable<TransitiveDependencies> dependencies)
{
var text = new StringBuilder();

foreach (var dependency in dependencies)
{
var (_, packages) = dependency;

var projectName = dependency.ProjectName;
var targetFramework = dependency.TargetFramework;

var vulnerablePackages = packages
.Select(item => item.Key)
.Where(item => item.IsVulnerable && item.VulnerabilityMitigation.IsNullOrEmpty())
.ToArray();

if (vulnerablePackages.Length == 0)
continue;

var header = $"{projectName}, {targetFramework}";

text.AppendLine(header)
.AppendLine(new string('-', header.Length));

foreach (var vulnerablePackage in vulnerablePackages)
{
PrintDependencyTree(text, vulnerablePackage, packages, 0);
}

text.AppendLine().AppendLine();
}

Clipboard.SetText(text.ToString());

ShowInfoBar("Dependency tree copied to clipboard", TimeSpan.FromSeconds(10));
}

private static void PrintDependencyTree(StringBuilder text, PackageInfo package, IReadOnlyDictionary<PackageInfo, HashSet<PackageInfo>> parentsByChild, int nesting)
private static void OpenDependencyTree()
{
var indent = new string(' ', nesting * 4);

text.Append(indent);
text.Append(package.PackageIdentity);

if (package.IsDeprecated)
text.Append(" - Deprecated");

if (package.IsOutdated)
text.Append(" - Outdated");

if (package.Vulnerabilities?.Count > 0)
{
text.Append(" - Vulnerable:");
foreach (var item in package.Vulnerabilities)
{
text.Append($" [ Severity: {item.Severity}, {item.AdvisoryUrl} ]");
}
}

text.AppendLine();

if (!parentsByChild.TryGetValue(package, out var dependsOn))
return;

foreach (var item in dependsOn)
{
PrintDependencyTree(text, item, parentsByChild, nesting + 1);
}
NuGetMonitorCommands.Instance?.ShowDependencyTreeToolWindow();
}

private static IEnumerable<string?> GetInfoTexts(IEnumerable<PackageReferenceInfo> topLevelPackageInfos)
Expand Down
144 changes: 100 additions & 44 deletions src/NuGetMonitor/View/Monitor/NugetMonitorViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Community.VisualStudio.Toolkit;
using DataGridExtensions;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using NuGet.Packaging.Core;
using NuGet.Versioning;
using NuGetMonitor.Abstractions;
using NuGetMonitor.Model.Models;
Expand All @@ -21,9 +24,10 @@
namespace NuGetMonitor.View.Monitor;

#pragma warning disable CA1812 // Avoid uninstantiated internal classes => used in xaml!

internal sealed partial class NuGetMonitorViewModel : INotifyPropertyChanged
{
private static readonly string[] _versionMetadataNames = { "Version", "VersionOverride" };
private static readonly string[] _versionMetadataNames = ["Version", "VersionOverride"];

private readonly ISolutionService _solutionService;

Expand Down Expand Up @@ -79,7 +83,7 @@ private async void Load()

var packages = packageReferences
.GroupBy(item => item.Identity)
.Select(group => new PackageViewModel(group, PackageItemType.PackageReference, _solutionService))
.Select(group => new PackageViewModel(this, group, PackageItemType.PackageReference, _solutionService))
.ToArray();

var packageIds = packages
Expand All @@ -93,7 +97,7 @@ private async void Load()
.SelectMany(project => project.Project.CentralVersionMap.Values.Select(item => new PackageReferenceEntry(item.EvaluatedInclude, item.GetVersion() ?? VersionRange.None, item, project, item.GetMetadataValue("Justification"), false)))
.Where(item => !packageIds.Contains(item.Identity.Id))
.GroupBy(item => item.Identity)
.Select(item => new PackageViewModel(item, PackageItemType.PackageVersion, _solutionService))
.Select(item => new PackageViewModel(this, item, PackageItemType.PackageVersion, _solutionService))
.ToArray();

Packages = packages.Concat(transitivePins).ToArray();
Expand Down Expand Up @@ -129,7 +133,7 @@ private void Refresh(DataGrid dataGrid)
Load();
}

public static void Update(PackageViewModel packageViewModel)
public void Update(PackageViewModel packageViewModel)
{
Update(new[] { packageViewModel });
}
Expand All @@ -139,60 +143,112 @@ private void UpdateSelected()
Update(SelectedPackages.ToArray());
}

private static void Update(ICollection<PackageViewModel> packageViewModels)
private async void Update(ICollection<PackageViewModel> packageViewModels)
{
using var projectCollection = new ProjectCollection();
try
{
IsLoading = true;

var packageReferencesByProject = packageViewModels
.Where(viewModel => viewModel.IsUpdateAvailable)
.SelectMany(viewModel => viewModel.Items.Select(item => new { item.Identity.Id, item.VersionSource, viewModel.ActiveVersion, item.VersionSource.GetContainingProject().FullPath, viewModel.SelectedVersion }))
.GroupBy(item => item.FullPath);
using var projectCollection = new ProjectCollection();

foreach (var packageReferenceEntries in packageReferencesByProject)
{
var fullPath = packageReferenceEntries.Key;
var viewModels = packageViewModels
.Where(viewModel => viewModel.IsUpdateAvailable && viewModel.SelectedVersion is not null)
.ToArray();

var project = ProjectRootElement.Open(fullPath, projectCollection, true);
if (!await AreTargetFrameworksCompatible(viewModels))
return;

var projectItems = project.Items;
var packageReferencesByProject = viewModels
.SelectMany(viewModel => viewModel.Items.Select(item => new { item.Identity.Id, item.VersionSource, viewModel.ActiveVersion, item.VersionSource.GetContainingProject().FullPath, viewModel.SelectedVersion }))
.GroupBy(item => item.FullPath);

foreach (var packageReferenceEntry in packageReferenceEntries)
foreach (var packageReferenceEntries in packageReferencesByProject)
{
var id = packageReferenceEntry.Id;
var selectedVersion = packageReferenceEntry.SelectedVersion;
var versionSource = packageReferenceEntry.VersionSource;
var currentVersion = packageReferenceEntry.ActiveVersion switch
{
NuGetVersion version => version.OriginalVersion,
VersionRange versionRange => versionRange.OriginalString,
_ => null
};

if (selectedVersion == null || currentVersion == null)
continue;

var metadataItems = projectItems
.Where(item => item.ItemType == versionSource.ItemType)
.Where(item => string.Equals(item.Include, id, StringComparison.OrdinalIgnoreCase))
.Select(item => item.Metadata.FirstOrDefault(metadata => _versionMetadataNames.Any(name => string.Equals(metadata.Name, name, StringComparison.OrdinalIgnoreCase))
&& string.Equals(metadata.Value, currentVersion, StringComparison.OrdinalIgnoreCase)))
.ExceptNullItems();

foreach (var metadata in metadataItems)
var fullPath = packageReferenceEntries.Key;

var project = ProjectRootElement.Open(fullPath, projectCollection, true);

var projectItems = project.Items;

foreach (var packageReferenceEntry in packageReferenceEntries)
{
metadata.Value = selectedVersion.ToString();
var id = packageReferenceEntry.Id;
var selectedVersion = packageReferenceEntry.SelectedVersion;
var versionSource = packageReferenceEntry.VersionSource;
var currentVersion = packageReferenceEntry.ActiveVersion switch
{
NuGetVersion version => version.OriginalVersion,
VersionRange versionRange => versionRange.OriginalString,
_ => null
};

if (selectedVersion == null || currentVersion == null)
continue;

var metadataItems = projectItems
.Where(item => item.ItemType == versionSource.ItemType)
.Where(item => string.Equals(item.Include, id, StringComparison.OrdinalIgnoreCase))
.Select(item => item.Metadata.FirstOrDefault(metadata => _versionMetadataNames.Any(name => string.Equals(metadata.Name, name, StringComparison.OrdinalIgnoreCase))
&& string.Equals(metadata.Value, currentVersion, StringComparison.OrdinalIgnoreCase)))
.ExceptNullItems();

foreach (var metadata in metadataItems)
{
metadata.Value = selectedVersion.ToString();
}
}

project.Save();
}

foreach (var packageViewModel in viewModels)
{
packageViewModel.ApplySelectedVersion();
}

project.Save();
ProjectService.ClearCache();
}
catch (OperationCanceledException)
{
// session cancelled
}
catch (Exception ex)
{
Log(LogLevel.Error, $"Updating package failed: {ex}");
}
finally
{
IsLoading = false;
}
}

foreach (var packageViewModel in packageViewModels)
private static async Task<bool> AreTargetFrameworksCompatible(PackageViewModel[] viewModels)
{
foreach (var viewModel in viewModels)
{
packageViewModel.ApplySelectedVersion();
var packageDetails = await NuGetService.GetPackageDetails(new PackageIdentity(viewModel.PackageReference.Id, viewModel.SelectedVersion));
if (packageDetails == null)
continue;

var projects = viewModel.Items.Select(referenceEntry => referenceEntry.ProjectItemInTargetFramework.Project);

var incompatibleProjects = projects
.Where(project => !packageDetails.SupportedFrameworks.Any(supportedFramework => project.TargetFramework.IsCompatibleWith(supportedFramework)))
.ToArray();

if (incompatibleProjects.Length <= 0)
continue;

var response = await VS.MessageBox.ShowAsync(
$"Package {packageDetails.Identity} ({string.Join(", ", packageDetails.SupportedFrameworks)}) is not compatible with projects {string.Join(",", incompatibleProjects.Select(p => p.NameAndFramework))}",
"Do you want to update anyway?",
OLEMSGICON.OLEMSGICON_WARNING, OLEMSGBUTTON.OLEMSGBUTTON_YESNO, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_SECOND);

if (response != VSConstants.MessageBoxResult.IDYES)
return false;
}

ProjectService.ClearCache();
return true;
}

private void NormalizePackageReferences()
Expand Down
Loading