diff --git a/src/Models/PackageReferenceEntry.cs b/src/Models/PackageReferenceEntry.cs index fedb4fd..c5030c3 100644 --- a/src/Models/PackageReferenceEntry.cs +++ b/src/Models/PackageReferenceEntry.cs @@ -1,12 +1,14 @@ -using NuGet.Versioning; +using Microsoft.Build.Evaluation; +using NuGet.Versioning; using NuGetMonitor.Services; namespace NuGetMonitor.Models; internal sealed record PackageReferenceEntry { - public PackageReferenceEntry(string id, VersionRange versionRange, ProjectItemInTargetFramework projectItemInTargetFramework, string justification) + public PackageReferenceEntry(string id, VersionRange versionRange, ProjectItem versionSource, ProjectItemInTargetFramework projectItemInTargetFramework, string justification) { + VersionSource = versionSource; ProjectItemInTargetFramework = projectItemInTargetFramework; Justification = justification; Identity = new PackageReference(id, versionRange); @@ -14,6 +16,8 @@ public PackageReferenceEntry(string id, VersionRange versionRange, ProjectItemIn public PackageReference Identity { get; } + public ProjectItem VersionSource { get; } + public ProjectItemInTargetFramework ProjectItemInTargetFramework { get; } public string Justification { get; } diff --git a/src/Services/ProjectService.cs b/src/Services/ProjectService.cs index 832109c..0bc702f 100644 --- a/src/Services/ProjectService.cs +++ b/src/Services/ProjectService.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.ObjectModel; +using System.IO; using Community.VisualStudio.Toolkit; using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; @@ -7,15 +8,58 @@ using NuGetMonitor.Models; using TomsToolbox.Essentials; using Project = Microsoft.Build.Evaluation.Project; +using ProjectItem = Microsoft.Build.Evaluation.ProjectItem; namespace NuGetMonitor.Services; -internal sealed record ProjectItemInTargetFramework(ProjectItem ProjectItem, NuGetFramework TargetFramework) +internal sealed class ProjectItemInTargetFramework { - public ProjectRootElement ContainingProject => ProjectItem.Xml.ContainingProject; + public ProjectItemInTargetFramework(ProjectItem projectItem, ProjectInTargetFramework project) + { + ProjectItem = projectItem; + Project = project; + } + + public ProjectItem ProjectItem { get; init; } + + public NuGetFramework TargetFramework => Project.TargetFramework; + + public ProjectInTargetFramework Project { get; } } -internal sealed record ProjectInTargetFramework(Project Project, NuGetFramework TargetFramework); +internal sealed class ProjectInTargetFramework +{ + private static readonly ReadOnlyDictionary _emptyVersionMap = new(new Dictionary()); + private static readonly DelegateEqualityComparer _itemIncludeComparer = new(item => item?.EvaluatedInclude.ToUpperInvariant()); + + public ProjectInTargetFramework(Project project, NuGetFramework targetFramework) + { + Project = project; + TargetFramework = targetFramework; + CentralVersionMap = GetCentralVersionMap(project); + } + + public Project Project { get; init; } + + public NuGetFramework TargetFramework { get; init; } + + public ReadOnlyDictionary CentralVersionMap { get; } + + private static ReadOnlyDictionary GetCentralVersionMap(Project project) + { + var useCentralPackageManagement = project.GetProperty("ManagePackageVersionsCentrally").IsTrue(); + + if (!useCentralPackageManagement) + return _emptyVersionMap; + + var versionMap = project + .GetItems("PackageVersion") + .Distinct(_itemIncludeComparer) + .ToDictionary(item => item.EvaluatedInclude, item => item); + + return new ReadOnlyDictionary(versionMap); + } +} internal static class ProjectService { @@ -48,7 +92,7 @@ public static async Task> GetPackageR return references .SelectMany(items => items) .OrderBy(item => item.Identity.Id) - .ThenBy(item => Path.GetFileName(item.ProjectItemInTargetFramework.ContainingProject.FullPath)) + .ThenBy(item => Path.GetFileName(item.VersionSource.GetContainingProject().FullPath)) .ToArray(); }); } @@ -74,12 +118,9 @@ public static ProjectInTargetFramework[] GetProjectsInTargetFramework(this Proje var projectCollection = _projectCollection; - lock (projectCollection) - { - return frameworks - .Select(framework => LoadProjectInTargetFramework(project, framework, projectCollection)) - .ToArray(); - } + return frameworks + .Select(framework => LoadProjectInTargetFramework(project, framework, projectCollection)) + .ToArray(); } private static ProjectInTargetFramework LoadProjectInTargetFramework(Project project, NuGetFramework framework, ProjectCollection projectCollection) @@ -109,16 +150,11 @@ private static IEnumerable GetPackageReferenceItem { try { - Project project; + var project = projectCollection.LoadProject(projectPath); - lock (projectCollection) - { - project = projectCollection.LoadProject(projectPath); - } + var frameworkSpecificProjects = project.GetProjectsInTargetFramework(); - var projects = project.GetProjectsInTargetFramework(); - - var allItems = projects.SelectMany(p => p.Project.GetItems("PackageReference").Select(item => new ProjectItemInTargetFramework(item, p.TargetFramework))); + var allItems = frameworkSpecificProjects.SelectMany(GetPackageReferenceItems); return allItems; } @@ -130,9 +166,18 @@ private static IEnumerable GetPackageReferenceItem } } + private static IEnumerable GetPackageReferenceItems(ProjectInTargetFramework frameworkSpecificProject) + { + var project = frameworkSpecificProject.Project; + + return project.GetItems("PackageReference") + .Select(item => new ProjectItemInTargetFramework(item, frameworkSpecificProject)); + } + private static PackageReferenceEntry? CreateEntry(ProjectItemInTargetFramework projectItemInTargetFramework) { var projectItem = projectItemInTargetFramework.ProjectItem; + var versionSource = projectItem; var id = projectItem.EvaluatedInclude; @@ -140,13 +185,35 @@ private static IEnumerable GetPackageReferenceItem if (id.Equals(NetStandardPackageId, StringComparison.OrdinalIgnoreCase)) return null; + var version = projectItem.GetVersion(); + var project = projectItemInTargetFramework.Project; + + if (version is null && project.CentralVersionMap.TryGetValue(id, out versionSource)) + { + version = versionSource.GetVersion(); + } + + return version is null + ? null + : new PackageReferenceEntry(id, version, versionSource, projectItemInTargetFramework, projectItem.GetMetadataValue("Justification")); + } + + internal static bool IsTrue(this ProjectProperty property) + { + return "true".Equals(property.EvaluatedValue, StringComparison.OrdinalIgnoreCase); + } + + internal static VersionRange? GetVersion(this ProjectItem projectItem) + { var versionValue = projectItem.GetMetadata("Version")?.EvaluatedValue; if (versionValue.IsNullOrEmpty()) return null; - return VersionRange.TryParse(versionValue, out var versionRange) - ? new PackageReferenceEntry(id, versionRange, projectItemInTargetFramework, projectItem.GetMetadataValue("Justification")) - : null; + return VersionRange.TryParse(versionValue, out var version) ? version : null; + } + internal static ProjectRootElement GetContainingProject(this ProjectItem projectItem) + { + return projectItem.Xml.ContainingProject; } } \ No newline at end of file diff --git a/src/View/Monitor/NugetMonitorViewModel.cs b/src/View/Monitor/NugetMonitorViewModel.cs index 5eea2a3..11cb813 100644 --- a/src/View/Monitor/NugetMonitorViewModel.cs +++ b/src/View/Monitor/NugetMonitorViewModel.cs @@ -120,7 +120,7 @@ private static void Update(ICollection packageViewModels) var packageReferencesByProject = packageViewModels .Where(viewModel => viewModel.IsUpdateAvailable) - .SelectMany(viewModel => viewModel.Items.Select(item => new { item.Identity, item.ProjectItemInTargetFramework.ContainingProject.FullPath, viewModel.SelectedVersion })) + .SelectMany(viewModel => viewModel.Items.Select(item => new { item.Identity, item.VersionSource, item.VersionSource.GetContainingProject().FullPath, viewModel.SelectedVersion })) .GroupBy(item => item.FullPath); foreach (var packageReferenceEntries in packageReferencesByProject) @@ -129,17 +129,19 @@ private static void Update(ICollection packageViewModels) var project = ProjectRootElement.Open(fullPath, projectCollection, true); - var packageReferences = project.Items; + var projectItems = project.Items; foreach (var packageReferenceEntry in packageReferenceEntries) { var identity = packageReferenceEntry.Identity; var selectedVersion = packageReferenceEntry.SelectedVersion; + var versionSource = packageReferenceEntry.VersionSource; if (selectedVersion == null) continue; - var metadataItems = packageReferences + var metadataItems = projectItems + .Where(item => item.ItemType == versionSource.ItemType) .Where(item => string.Equals(item.Include, identity.Id, StringComparison.OrdinalIgnoreCase)) .Select(item => item.Metadata.FirstOrDefault(metadata => string.Equals(metadata.Name, "Version", StringComparison.OrdinalIgnoreCase) && string.Equals(metadata.Value, identity.VersionRange.OriginalString, StringComparison.OrdinalIgnoreCase))) @@ -148,7 +150,6 @@ private static void Update(ICollection packageViewModels) foreach (var metadata in metadataItems) { metadata.Value = selectedVersion.ToString(); - metadata.ExpressedAsAttribute = true; } } diff --git a/src/View/PackageViewModel.cs b/src/View/PackageViewModel.cs index bf36684..6b03752 100644 --- a/src/View/PackageViewModel.cs +++ b/src/View/PackageViewModel.cs @@ -14,10 +14,9 @@ public PackageViewModel(IGrouping items { Items = items; PackageReference = items.Key; - Projects = items.GroupBy(item => item.ProjectItemInTargetFramework.ContainingProject).Select(item => new ProjectViewModel(item.Key)).ToArray(); + Projects = items.GroupBy(item => item.ProjectItemInTargetFramework.ProjectItem.GetContainingProject()).Select(item => new ProjectViewModel(item.Key)).ToArray(); ActiveVersion = NuGetVersion.TryParse(PackageReference.VersionRange.OriginalString, out var simpleVersion) ? simpleVersion : PackageReference.VersionRange; Justifications = string.Join(", ", Items.Select(reference => reference.Justification).Distinct()); - } public IGrouping Items { get; }