Skip to content

Commit

Permalink
Merge pull request #52 from tom-englert/dev/issue#50
Browse files Browse the repository at this point in the history
Dev/issue#50
  • Loading branch information
sboulema authored Dec 1, 2024
2 parents dcc0d03 + 973c95a commit c9830d8
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 228 deletions.
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

0 comments on commit c9830d8

Please sign in to comment.