From 29ce4a3db103750157ac78b5c56d6a2617a39e27 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:06:13 +1000 Subject: [PATCH] refactor to handle multiple projects --- .../Features/Evaluation/ProjectEvaluation.cs | 134 ++++++++++-------- .../Features/Nuget/NugetClientService.cs | 8 +- .../Features/Nuget/NugetPanel.cs | 9 +- .../Features/Nuget/PackageEntry.cs | 22 ++- 4 files changed, 99 insertions(+), 74 deletions(-) diff --git a/src/SharpIDE.Application/Features/Evaluation/ProjectEvaluation.cs b/src/SharpIDE.Application/Features/Evaluation/ProjectEvaluation.cs index 84759dc..e5f1816 100644 --- a/src/SharpIDE.Application/Features/Evaluation/ProjectEvaluation.cs +++ b/src/SharpIDE.Application/Features/Evaluation/ProjectEvaluation.cs @@ -57,86 +57,102 @@ public static class ProjectEvaluation return Guid.Parse(userSecretsId); } - public static async Task> GetPackageReferencesForProject(SharpIdeProjectModel projectModel, bool includeTransitive = true) + public static async Task> GetPackageReferencesForProjects(List projectModels, bool includeTransitive = true) { - using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(ProjectEvaluation)}.{nameof(GetPackageReferencesForProject)}"); - Guard.Against.Null(projectModel, nameof(projectModel)); + using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(ProjectEvaluation)}.{nameof(GetPackageReferencesForProjects)}"); + Guard.Against.Null(projectModels, nameof(projectModels)); - var project = _projectCollection.GetLoadedProjects(projectModel.FilePath).Single(); - - var assetsPath = project.GetPropertyValue("ProjectAssetsFile"); - - if (File.Exists(assetsPath) is false) + var projects = projectModels.Select(s => { - throw new FileNotFoundException("Could not find project.assets.json file", assetsPath); - } - var lockFileFormat = new LockFileFormat(); - var lockFile = lockFileFormat.Read(assetsPath); - var packages = GetPackagesFromAssetsFile(lockFile, includeTransitive); - return packages; + var proj = _projectCollection.GetLoadedProjects(s.FilePath).Single(); + var assetsPath = proj.GetPropertyValue("ProjectAssetsFile"); + + if (File.Exists(assetsPath) is false) + { + throw new FileNotFoundException("Could not find project.assets.json file", assetsPath); + } + var lockFileFormat = new LockFileFormat(); + var lockFile = lockFileFormat.Read(assetsPath); + return (LockFile: lockFile, Project: s); + }).ToList(); + + var result = await GetPackagesFromAssetsFiles(projects); + return result; } - // ChatGPT Special - private static List GetPackagesFromAssetsFile(LockFile assetsFile, bool includeTransitive) + public static async Task> GetPackagesFromAssetsFiles(List<(LockFile, SharpIdeProjectModel)> projects, bool includeTransitive = true) { - var packages = new List(); - var dependencyMap = NugetDependencyGraph.GetPackageDependencyMap(assetsFile); - - // We currently do not handle multi-targeted projects - var target = assetsFile.Targets.SingleOrDefault(t => t.RuntimeIdentifier == null); - if (target == null) return packages; - - var tfm = target.TargetFramework.GetShortFolderName(); - var tfmInfo = assetsFile.PackageSpec.TargetFrameworks - .FirstOrDefault(t => t.FrameworkName.Equals(target.TargetFramework)); - - if (tfmInfo == null) return packages; - - var topLevelDependencies = tfmInfo.Dependencies - .DistinctBy(s => s.Name) - .Select(s => s.Name) - .ToHashSet(); - - foreach (var lockFileTargetLibrary in target.Libraries.Where(l => l.Type == "package")) + using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(ProjectEvaluation)}.{nameof(GetPackagesFromAssetsFiles)}"); + var allPackages = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var (assetsFile, project) in projects) { - var isTopLevel = topLevelDependencies.Contains(lockFileTargetLibrary.Name); - if (!includeTransitive && !isTopLevel) continue; + var dependencyMap = NugetDependencyGraph.GetPackageDependencyMap(assetsFile); - var dependency = tfmInfo.Dependencies - .FirstOrDefault(d => d.Name.Equals(lockFileTargetLibrary.Name, StringComparison.OrdinalIgnoreCase)); + // We currently do not handle multi-targeted projects + var target = assetsFile.Targets.SingleOrDefault(t => t.RuntimeIdentifier == null); + if (target == null) continue; - var dependents = dependencyMap.GetValueOrDefault(lockFileTargetLibrary.Name, []); - var mappedDependents = dependents.Select(d => new DependentPackage + var tfm = target.TargetFramework.GetShortFolderName(); + var tfmInfo = assetsFile.PackageSpec.TargetFrameworks + .FirstOrDefault(t => t.FrameworkName.Equals(target.TargetFramework)); + + if (tfmInfo == null) continue; + + var topLevelDependencies = tfmInfo.Dependencies + .DistinctBy(s => s.Name) + .Select(s => s.Name) + .ToHashSet(); + + foreach (var lockFileTargetLibrary in target.Libraries.Where(l => l.Type == "package")) { - PackageName = d.PackageName, - RequestedVersion = d.PackageDependency.VersionRange - }).ToList(); + if (string.IsNullOrEmpty(lockFileTargetLibrary.Name)) continue; - packages.Add(new InstalledPackage - { - Name = lockFileTargetLibrary.Name, - RequestedVersion = dependency?.LibraryRange.VersionRange?.ToString() ?? "", - ResolvedVersion = lockFileTargetLibrary.Version?.ToString(), - TargetFramework = tfm, - IsTopLevel = isTopLevel, - IsAutoReferenced = dependency?.AutoReferenced ?? false, - DependentPackages = mappedDependents - }); + var isTopLevel = topLevelDependencies.Contains(lockFileTargetLibrary.Name); + if (!includeTransitive && !isTopLevel) continue; + + var dependency = tfmInfo.Dependencies + .FirstOrDefault(d => d.Name.Equals(lockFileTargetLibrary.Name, StringComparison.OrdinalIgnoreCase)); + + var dependents = dependencyMap.GetValueOrDefault(lockFileTargetLibrary.Name, []); + var mappedDependents = dependents.Select(d => new DependentPackage + { + PackageName = d.PackageName, + RequestedVersion = d.PackageDependency.VersionRange + }).ToList(); + + var existingPackage = allPackages.GetValueOrDefault(lockFileTargetLibrary.Name) ?? new InstalledPackage { Name = lockFileTargetLibrary.Name, ProjectPackageReferences = [] }; + existingPackage.ProjectPackageReferences.Add(new ProjectPackageReference + { + Project = project, + InstalledVersion = lockFileTargetLibrary.Version, + IsTopLevel = isTopLevel, + IsAutoReferenced = dependency?.AutoReferenced ?? false, + DependentPackages = mappedDependents + }); + allPackages[lockFileTargetLibrary.Name] = existingPackage; + } } - - return packages; + return allPackages.Values.ToList(); } +} public class InstalledPackage { public required string Name { get; set; } - public required string RequestedVersion { get; set; } - public required string? ResolvedVersion { get; set; } - public required string TargetFramework { get; set; } + //public required NuGetVersion LatestVersion { get; set; } + public required List ProjectPackageReferences { get; set; } +} + +public class ProjectPackageReference +{ + public required SharpIdeProjectModel Project { get; set; } + public required NuGetVersion InstalledVersion { get; set; } public required bool IsTopLevel { get; set; } public required bool IsAutoReferenced { get; set; } public List? DependentPackages { get; set; } + public bool IsTransitive => !IsTopLevel && !IsAutoReferenced; } + public class DependentPackage { public required string PackageName { get; set; } diff --git a/src/SharpIDE.Application/Features/Nuget/NugetClientService.cs b/src/SharpIDE.Application/Features/Nuget/NugetClientService.cs index b0c01e9..662daae 100644 --- a/src/SharpIDE.Application/Features/Nuget/NugetClientService.cs +++ b/src/SharpIDE.Application/Features/Nuget/NugetClientService.cs @@ -9,7 +9,7 @@ using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; namespace SharpIDE.Application.Features.Nuget; -public record InstalledNugetPackageInfo(bool IsTransitive, NuGetVersion Version, List? DependentPackages); +public record InstalledNugetPackageInfo(List ProjectPackageReferences); public record IdePackageResult(string PackageId, List PackageFromSources, InstalledNugetPackageInfo? InstalledNugetPackageInfo); public record struct IdePackageFromSourceResult(IPackageSearchMetadata PackageSearchMetadata, PackageSource Source); public class NugetClientService @@ -118,11 +118,7 @@ public class NugetClientService var packagesResult = new List(); foreach (var installedPackage in installedPackages) { - var isTransitive = installedPackage.IsTopLevel is false; - var nugetVersionString = installedPackage.ResolvedVersion ?? installedPackage.RequestedVersion; - var nugetVersion = NuGetVersion.Parse(nugetVersionString); - - var installedNugetPackageInfo = new InstalledNugetPackageInfo(isTransitive, nugetVersion, installedPackage.DependentPackages); + var installedNugetPackageInfo = new InstalledNugetPackageInfo(installedPackage.ProjectPackageReferences); var idePackageResult = new IdePackageResult(installedPackage.Name, [], installedNugetPackageInfo); foreach (var source in packageSources) diff --git a/src/SharpIDE.Godot/Features/Nuget/NugetPanel.cs b/src/SharpIDE.Godot/Features/Nuget/NugetPanel.cs index 276ed2c..6ec7a77 100644 --- a/src/SharpIDE.Godot/Features/Nuget/NugetPanel.cs +++ b/src/SharpIDE.Godot/Features/Nuget/NugetPanel.cs @@ -26,8 +26,7 @@ public partial class NugetPanel : Control private readonly PackedScene _packageEntryScene = ResourceLoader.Load("uid://cqc2xlt81ju8s"); private readonly Texture2D _csprojIcon = ResourceLoader.Load("uid://cqt30ma6xgder"); - - private IdePackageResult? _selectedPackage; + // we use this to access the project for the dropdown private List _projects = null!; @@ -81,7 +80,6 @@ public partial class NugetPanel : Control private async Task OnPackageSelected(IdePackageResult packageResult) { - _selectedPackage = packageResult; await _nugetPackageDetails.SetPackage(packageResult); } @@ -113,6 +111,7 @@ public partial class NugetPanel : Control private async Task PopulateSearchResults() { + return; var result = await _nugetClientService.GetTop100Results(_solution!.DirectoryPath); var scenes = result.Select(s => { @@ -135,7 +134,7 @@ public partial class NugetPanel : Control { var project = _solution!.AllProjects.First(s => s.Name == "ProjectA"); await project.MsBuildEvaluationProjectTask; - var installedPackages = await ProjectEvaluation.GetPackageReferencesForProject(project); + var installedPackages = await ProjectEvaluation.GetPackageReferencesForProjects([project]); var idePackageResult = await _nugetClientService.GetPackagesForInstalledPackages(project.ChildNodeBasePath, installedPackages); var scenes = idePackageResult.Select(s => { @@ -144,7 +143,7 @@ public partial class NugetPanel : Control scene.PackageSelected += OnPackageSelected; return scene; }).ToList(); - var transitiveScenes = scenes.Where(s => s.PackageResult.InstalledNugetPackageInfo!.IsTransitive).ToList(); + var transitiveScenes = scenes.Where(s => s.PackageResult.InstalledNugetPackageInfo!.ProjectPackageReferences.Any(x => x.IsTransitive)).ToList(); var directScenes = scenes.Except(transitiveScenes).ToList(); await this.InvokeAsync(() => { diff --git a/src/SharpIDE.Godot/Features/Nuget/PackageEntry.cs b/src/SharpIDE.Godot/Features/Nuget/PackageEntry.cs index 947076b..f02bd86 100644 --- a/src/SharpIDE.Godot/Features/Nuget/PackageEntry.cs +++ b/src/SharpIDE.Godot/Features/Nuget/PackageEntry.cs @@ -52,9 +52,10 @@ public partial class PackageEntry : MarginContainer if (PackageResult is null) return; _packageNameLabel.Text = PackageResult.PackageId; var installedPackagedInfo = PackageResult.InstalledNugetPackageInfo; - if (installedPackagedInfo?.DependentPackages is not null && installedPackagedInfo.IsTransitive) + var isTransitive = installedPackagedInfo?.ProjectPackageReferences.Any(p => p.IsTransitive) ?? false; + if (isTransitive && installedPackagedInfo?.ProjectPackageReferences.Any(p => p.DependentPackages?.Count is not 0) is true) { - var transitiveOriginsGroupedByVersion = installedPackagedInfo.DependentPackages + var transitiveOriginsGroupedByVersion = installedPackagedInfo.ProjectPackageReferences.SelectMany(s => s.DependentPackages ?? []) .GroupBy(t => t.RequestedVersion) .Select(g => new { @@ -67,10 +68,10 @@ public partial class PackageEntry : MarginContainer {string.Join("\n", transitiveOriginsGroupedByVersion.Select(t => $"{t.RequestedVersion.ToString("p", VersionRangeFormatter.Instance)} by {string.Join(", ", t.PackageNames)}"))} """; } - _installedVersionLabel.Text = installedPackagedInfo?.IsTransitive is true ? $"({installedPackagedInfo?.Version.ToNormalizedString()})" : installedPackagedInfo?.Version.ToNormalizedString(); + _installedVersionLabel.Text = GetInstalledVersionsText(installedPackagedInfo); var highestVersionPackageFromSource = PackageResult.PackageFromSources .MaxBy(p => p.PackageSearchMetadata.Identity.Version); - if (installedPackagedInfo?.Version != highestVersionPackageFromSource.PackageSearchMetadata.Identity.Version) + if (installedPackagedInfo?.ProjectPackageReferences.TrueForAll(s => s.InstalledVersion != highestVersionPackageFromSource.PackageSearchMetadata.Identity.Version) is true) { _latestVersionLabel.Text = highestVersionPackageFromSource.PackageSearchMetadata.Identity.Version.ToNormalizedString(); } @@ -94,4 +95,17 @@ public partial class PackageEntry : MarginContainer _sourceNamesContainer.AddChild(label); } } + + private string GetInstalledVersionsText(InstalledNugetPackageInfo? packageInfo) + { + if (packageInfo is null) return string.Empty; + + var versions = packageInfo.ProjectPackageReferences + .Select(p => p.InstalledVersion.ToNormalizedString()) + .Distinct() + .ToList(); + var isTransitive = packageInfo.ProjectPackageReferences.Any(p => p.IsTransitive); + var text = isTransitive ? $"({string.Join(", ", versions)})" : string.Join(", ", versions); + return text; + } } \ No newline at end of file