From 8e1762c3b9f2f19e1a8469b6f7202f2e5b314bf1 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 4 Mar 2024 21:30:47 -0800 Subject: [PATCH 01/26] Move to central package management --- .../lib/NuGetUpdater/Directory.Packages.props | 28 ++++++++++ .../NuGetUpdater.Cli.Test.csproj | 6 +-- .../NuGetUpdater.Cli/NuGetUpdater.Cli.csproj | 2 +- .../NuGetUpdater.Core.Test.csproj | 6 +-- .../TemporaryDirectory.cs | 36 +++++++++++-- .../Update/UpdateWorkerTestBase.cs | 52 +++++-------------- .../Utilities/SdkPackageUpdaterHelperTests.cs | 10 ++-- .../NuGetUpdater.Core.csproj | 12 ++--- 8 files changed, 91 insertions(+), 61 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/Directory.Packages.props diff --git a/nuget/helpers/lib/NuGetUpdater/Directory.Packages.props b/nuget/helpers/lib/NuGetUpdater/Directory.Packages.props new file mode 100644 index 00000000000..ccb4ce00616 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/Directory.Packages.props @@ -0,0 +1,28 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/NuGetUpdater.Cli.Test.csproj b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/NuGetUpdater.Cli.Test.csproj index 80c9b7c1dc5..309d0e37391 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/NuGetUpdater.Cli.Test.csproj +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/NuGetUpdater.Cli.Test.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/NuGetUpdater.Cli.csproj b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/NuGetUpdater.Cli.csproj index ede81608746..ab2796fd1de 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/NuGetUpdater.Cli.csproj +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/NuGetUpdater.Cli.csproj @@ -10,7 +10,7 @@ - + diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj index 0f13b594c31..6201ed95d9c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj @@ -11,9 +11,9 @@ - - - + + + diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs index c1cfde6ddc5..3c74ce79596 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; + +using TestFile = (string Path, string Contents); namespace NuGetUpdater.Core.Test; @@ -20,15 +24,41 @@ public void Dispose() Directory.Delete(DirectoryPath, true); } - public static TemporaryDirectory CreateWithContents(params (string Path, string Contents)[] fileContents) + public async Task ReadFileContentsAsync(HashSet filePaths) + { + var files = new List<(string Path, string Content)>(); + foreach (var file in Directory.GetFiles(DirectoryPath, "*.*", SearchOption.AllDirectories)) + { + var localPath = file.StartsWith(DirectoryPath) + ? file[DirectoryPath.Length..] + : file; // how did this happen? + localPath = localPath.NormalizePathToUnix(); + if (localPath.StartsWith('/')) + { + localPath = localPath[1..]; + } + + if (filePaths.Contains(localPath)) + { + var content = await File.ReadAllTextAsync(file); + files.Add((localPath, content)); + } + } + + return files.ToArray(); + } + + public static async Task CreateWithContentsAsync(params TestFile[] fileContents) { var temporaryDirectory = new TemporaryDirectory(); + foreach (var (path, contents) in fileContents) { - var fullPath = Path.Combine(temporaryDirectory.DirectoryPath, path); + var localPath = path.StartsWith('/') ? path[1..] : path; // remove path rooting character + var fullPath = Path.Combine(temporaryDirectory.DirectoryPath, localPath); var fullDirectory = Path.GetDirectoryName(fullPath)!; Directory.CreateDirectory(fullDirectory); - File.WriteAllText(fullPath, contents); + await File.WriteAllTextAsync(fullPath, contents); } return temporaryDirectory; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs index fb1f959deb7..8de6731fede 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs @@ -1,16 +1,15 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Xunit; -using TestFile = (string Path, string Content); -using TestProject = (string Path, string Content, System.Guid ProjectId); - namespace NuGetUpdater.Core.Test.Update; +using TestFile = (string Path, string Content); +using TestProject = (string Path, string Content, Guid ProjectId); + public abstract class UpdateWorkerTestBase { protected static Task TestNoChange( @@ -20,7 +19,7 @@ protected static Task TestNoChange( bool useSolution, string projectContents, bool isTransitive = false, - (string Path, string Content)[]? additionalFiles = null, + TestFile[]? additionalFiles = null, string projectFilePath = "test-project.csproj") { return useSolution @@ -67,7 +66,7 @@ protected static Task TestNoChangeforProject( string newVersion, string projectContents, bool isTransitive = false, - (string Path, string Content)[]? additionalFiles = null, + TestFile[]? additionalFiles = null, string projectFilePath = "test-project.csproj") => TestUpdateForProject( dependencyName, @@ -202,53 +201,26 @@ protected static async Task TestUpdateForSolution( AssertContainsFiles(expectedResult, actualResult); } - protected static async Task<(string Path, string Content)[]> RunUpdate((string Path, string Content)[] files, Func action) + protected static async Task RunUpdate(TestFile[] files, Func action) { // write initial files - using var tempDir = new TemporaryDirectory(); - foreach (var file in files) - { - var localPath = file.Path.StartsWith('/') ? file.Path[1..] : file.Path; // remove path rooting character - var filePath = Path.Combine(tempDir.DirectoryPath, localPath); - var directoryPath = Path.GetDirectoryName(filePath); - Directory.CreateDirectory(directoryPath!); - await File.WriteAllTextAsync(filePath, file.Content); - } + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files); // run update - await action(tempDir.DirectoryPath); + await action(temporaryDirectory.DirectoryPath); // gather results - var expectedFiles = new HashSet(files.Select(f => f.Path)); - var result = new List<(string Path, string Content)>(); - foreach (var file in Directory.GetFiles(tempDir.DirectoryPath, "*.*", SearchOption.AllDirectories)) - { - var localPath = file.StartsWith(tempDir.DirectoryPath) - ? file[tempDir.DirectoryPath.Length..] - : file; // how did this happen? - localPath = localPath.NormalizePathToUnix(); - if (localPath.StartsWith('/')) - { - localPath = localPath[1..]; - } - - if (expectedFiles.Contains(localPath)) - { - var content = await File.ReadAllTextAsync(file); - result.Add((localPath, content)); - } - } - - return result.ToArray(); + var filePaths = files.Select(f => f.Path).ToHashSet(); + return await temporaryDirectory.ReadFileContentsAsync(filePaths); } - protected static void AssertEqualFiles((string Path, string Content)[] expected, (string Path, string Content)[] actual) + protected static void AssertEqualFiles(TestFile[] expected, TestFile[] actual) { Assert.Equal(expected.Length, actual.Length); AssertContainsFiles(expected, actual); } - protected static void AssertContainsFiles((string Path, string Content)[] expected, (string Path, string Content)[] actual) + protected static void AssertContainsFiles(TestFile[] expected, TestFile[] actual) { var actualContents = actual.ToDictionary(pair => pair.Path, pair => pair.Content); foreach (var expectedPair in expected) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs index bef7bd6abca..6c735c737c4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs @@ -11,7 +11,7 @@ public class SdkPackageUpdaterHelperTests [Fact] public async Task DirectoryBuildFilesAreOnlyPulledInFromParentDirectories() { - using var temporaryDirectory = TemporaryDirectory.CreateWithContents( + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync( ("src/SomeProject.csproj", """ @@ -52,7 +52,7 @@ public async Task DirectoryBuildFilesAreOnlyPulledInFromParentDirectories() [InlineData("src/", "")] // project in subdirectory, global.json at root public async Task BuildFileEnumerationWorksEvenWithNonSupportedSdkInGlobalJson(string projectSubpath, string globalJsonSubpath) { - using var temporaryDirectory = TemporaryDirectory.CreateWithContents( + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync( ($"{projectSubpath}SomeProject.csproj", """ @@ -82,7 +82,7 @@ public async Task BuildFileEnumerationWorksEvenWithNonSupportedSdkInGlobalJson(s [Fact] public async Task BuildFileEnumerationWithNonStandardMSBuildSdkAndNonSupportedSdkVersionInGlobalJson() { - using var temporaryDirectory = TemporaryDirectory.CreateWithContents( + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync( ("global.json", """ { "sdk": { @@ -123,7 +123,7 @@ public async Task BuildFileEnumerationWithNonStandardMSBuildSdkAndNonSupportedSd [Fact] public async Task BuildFileEnumerationWithUnsuccessfulImport() { - using var temporaryDirectory = TemporaryDirectory.CreateWithContents( + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync( ("Directory.Build.props", """ @@ -146,7 +146,7 @@ public async Task BuildFileEnumerationWithUnsuccessfulImport() [Fact] public async Task BuildFileEnumerationWithGlobalJsonWithComments() { - using var temporaryDirectory = TemporaryDirectory.CreateWithContents( + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync( ("global.json", """ { // this is a comment diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj index 25be06f42a3..faa7e17bb51 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + From bbb5305bae44cec8cc9133a1fb61afce3e507586 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Fri, 8 Mar 2024 14:56:39 -0800 Subject: [PATCH 02/26] Bring GlobalJsonUpdater into alignment with DotNetToolsJsonUpdater --- .../Updater/DotNetToolsJsonUpdater.cs | 48 ++++++++----------- .../Updater/GlobalJsonUpdater.cs | 17 ++++--- .../Updater/UpdaterWorker.cs | 11 +---- .../Utilities/MSBuildHelper.cs | 9 +++- 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs index a7aa673a6be..bf838aab415 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; @@ -10,50 +9,43 @@ internal static class DotNetToolsJsonUpdater public static async Task UpdateDependencyAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, Logger logger) { - var buildFiles = LoadBuildFiles(repoRootPath, workspacePath, logger); - if (buildFiles.Length == 0) + var dotnetToolsJsonFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); + if (dotnetToolsJsonFile is null) { logger.Log(" No dotnet-tools.json files found."); return; } - logger.Log(" Updating dotnet-tools.json files."); + logger.Log($" Updating [{dotnetToolsJsonFile.RepoRelativePath}] file."); - - var filesToUpdate = buildFiles.Where(f => - f.GetDependencies().Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase))) - .ToImmutableArray(); - if (filesToUpdate.Length == 0) + var containsDependency = dotnetToolsJsonFile.GetDependencies().Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)); + if (!containsDependency) { - logger.Log($" Dependency [{dependencyName}] not found in any dotnet-tools.json files."); + logger.Log($" Dependency [{dependencyName}] not found."); return; } - foreach (var buildFile in filesToUpdate) - { - var tool = buildFile.Tools - .Single(kvp => kvp.Key.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)); + var tool = dotnetToolsJsonFile.Tools + .Single(kvp => kvp.Key.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)); - var toolObject = tool.Value?.AsObject(); + var toolObject = tool.Value?.AsObject(); - if (toolObject is not null && - toolObject["version"]?.GetValue() == previousDependencyVersion) - { - buildFile.UpdateProperty(["tools", dependencyName, "version"], newDependencyVersion); + if (toolObject is not null && + toolObject["version"]?.GetValue() == previousDependencyVersion) + { + dotnetToolsJsonFile.UpdateProperty(["tools", dependencyName, "version"], newDependencyVersion); - if (await buildFile.SaveAsync()) - { - logger.Log($" Saved [{buildFile.RepoRelativePath}]."); - } + if (await dotnetToolsJsonFile.SaveAsync()) + { + logger.Log($" Saved [{dotnetToolsJsonFile.RepoRelativePath}]."); } } } - private static ImmutableArray LoadBuildFiles(string repoRootPath, string workspacePath, Logger logger) + private static DotNetToolsJsonBuildFile? TryLoadBuildFile(string repoRootPath, string workspacePath, Logger logger) { - var dotnetToolsJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./.config/dotnet-tools.json"); - return dotnetToolsJsonPath is not null - ? [DotNetToolsJsonBuildFile.Open(repoRootPath, dotnetToolsJsonPath, logger)] - : []; + return MSBuildHelper.GetDotNetToolsJsonPath(repoRootPath, workspacePath) is { } dotnetToolsJsonPath + ? DotNetToolsJsonBuildFile.Open(repoRootPath, dotnetToolsJsonPath, logger) + : null; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs index fa8d9aa99ed..7a6b1b74962 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using System.Threading.Tasks; @@ -9,20 +8,19 @@ internal static class GlobalJsonUpdater { public static async Task UpdateDependencyAsync( string repoRootPath, - string globalJsonPath, + string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, Logger logger) { - if (!File.Exists(globalJsonPath)) + var globalJsonFile = LoadBuildFile(repoRootPath, workspacePath, logger); + if (globalJsonFile is null) { - logger.Log($" No global.json file found at [{globalJsonPath}]."); + logger.Log(" No global.json files found."); return; } - var globalJsonFile = GlobalJsonBuildFile.Open(repoRootPath, globalJsonPath, logger); - logger.Log($" Updating [{globalJsonFile.RepoRelativePath}] file."); var containsDependency = globalJsonFile.GetDependencies().Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)); @@ -51,4 +49,11 @@ public static async Task UpdateDependencyAsync( logger.Log($" Saved [{globalJsonFile.RepoRelativePath}]."); } } + + private static GlobalJsonBuildFile? LoadBuildFile(string repoRootPath, string workspacePath, Logger logger) + { + return MSBuildHelper.GetGlobalJsonPath(repoRootPath, workspacePath) is { } globalJsonPath + ? GlobalJsonBuildFile.Open(repoRootPath, globalJsonPath, logger) + : null; + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs index af613e6b22d..0259aacc6d2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs @@ -9,7 +9,6 @@ namespace NuGetUpdater.Core; public class UpdaterWorker { private readonly Logger _logger; - private readonly HashSet _processedGlobalJsonPaths = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _processedProjectPaths = new(StringComparer.OrdinalIgnoreCase); public UpdaterWorker(Logger logger) @@ -29,6 +28,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string dep if (!isTransitive) { await DotNetToolsJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger); + await GlobalJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger); } var extension = Path.GetExtension(workspacePath).ToLowerInvariant(); @@ -50,7 +50,6 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string dep break; } - _processedGlobalJsonPaths.Clear(); _processedProjectPaths.Clear(); } @@ -139,14 +138,6 @@ private async Task RunUpdaterAsync( _logger.Log($"Updating project [{projectPath}]"); - if (!isTransitive - && MSBuildHelper.GetGlobalJsonPath(repoRootPath, projectPath) is { } globalJsonPath - && !_processedGlobalJsonPaths.Contains(globalJsonPath)) - { - _processedGlobalJsonPaths.Add(globalJsonPath); - await GlobalJsonUpdater.UpdateDependencyAsync(repoRootPath, globalJsonPath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger); - } - if (NuGetHelper.HasPackagesConfigFile(projectPath)) { await PackagesConfigUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, _logger); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index daf9372b186..f911a791e36 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -483,9 +483,14 @@ internal static async Task GetAllPackageDependenciesAsync( } } - internal static string? GetGlobalJsonPath(string repoRootPath, string projectPath) + internal static string? GetGlobalJsonPath(string repoRootPath, string workspacePath) { - return PathHelper.GetFileInDirectoryOrParent(Path.GetDirectoryName(projectPath)!, repoRootPath, "global.json"); + return PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json"); + } + + internal static string? GetDotNetToolsJsonPath(string repoRootPath, string workspacePath) + { + return PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./.config/dotnet-tools.json"); } internal static async Task> LoadBuildFiles(string repoRootPath, string projectPath) From 39dae40b9a7b8370648a99ce35dfd8159822fa12 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 11 Mar 2024 22:57:28 -0700 Subject: [PATCH 03/26] Enable implicit usings --- nuget/helpers/lib/NuGetUpdater/Directory.Common.props | 1 + 1 file changed, 1 insertion(+) diff --git a/nuget/helpers/lib/NuGetUpdater/Directory.Common.props b/nuget/helpers/lib/NuGetUpdater/Directory.Common.props index 4cd2d3efd0f..7e6d11c8964 100644 --- a/nuget/helpers/lib/NuGetUpdater/Directory.Common.props +++ b/nuget/helpers/lib/NuGetUpdater/Directory.Common.props @@ -10,6 +10,7 @@ 2. Update tests as needed at `NuGetUpdater\NuGetUpdater.Core.Test\FrameworkChecker\CompatibilityCheckerFacts.cs` --> net8.0 + enable true $(MSBuildThisFileDirectory)artifacts From 2c805e72479048ea69f6fff850237890f915661c Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 11 Mar 2024 22:58:04 -0700 Subject: [PATCH 04/26] Add some test helpers --- .../NuGetUpdater.Core.Test.csproj | 1 + .../Utilities/AssertEx.cs | 250 ++++++++++++++++ .../Utilities/DiffUtil.cs | 266 ++++++++++++++++++ 3 files changed, 517 insertions(+) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/DiffUtil.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj index 6201ed95d9c..62a33812dd6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj @@ -11,6 +11,7 @@ + diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs new file mode 100644 index 00000000000..033c844f9ea --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs @@ -0,0 +1,250 @@ +#nullable disable + +using System.Collections; +using System.Collections.Immutable; +using System.Reflection; +using System.Text; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Utilities; + +/// +/// Assert style type to deal with the lack of features in xUnit's Assert type +/// +public static class AssertEx +{ + public static void Equal( + ImmutableArray expected, + ImmutableArray actual, + IEqualityComparer comparer = null, + string message = null) + { + Equal(expected, (IEnumerable)actual, comparer, message); + } + + public static void Equal( + ImmutableArray expected, + IEnumerable actual, + IEqualityComparer comparer = null, + string message = null) + { + if (actual == null || expected.IsDefault) + { + Assert.True((actual == null) == expected.IsDefault, message); + } + else + { + Equal((IEnumerable)expected, actual, comparer, message); + } + } + + public static void Equal( + IEnumerable expected, + ImmutableArray actual, + IEqualityComparer comparer = null, + string message = null) + { + if (expected == null || actual.IsDefault) + { + Assert.True((expected == null) == actual.IsDefault, message); + } + else + { + Equal(expected, (IEnumerable)actual, comparer, message); + } + } + + public static void Equal( + IEnumerable expected, + IEnumerable actual, + IEqualityComparer comparer = null, + string message = null) + { + if (expected == null) + { + Assert.Null(actual); + } + else + { + Assert.NotNull(actual); + } + + if (SequenceEqual(expected, actual, comparer)) + { + return; + } + + Assert.True(false, GetAssertMessage(expected, actual, comparer, message)); + } + + private static bool SequenceEqual( + IEnumerable expected, + IEnumerable actual, + IEqualityComparer comparer = null) + { + if (ReferenceEquals(expected, actual)) + { + return true; + } + + var enumerator1 = expected.GetEnumerator(); + var enumerator2 = actual.GetEnumerator(); + + while (true) + { + var hasNext1 = enumerator1.MoveNext(); + var hasNext2 = enumerator2.MoveNext(); + + if (hasNext1 != hasNext2) + { + return false; + } + + if (!hasNext1) + { + break; + } + + var value1 = enumerator1.Current; + var value2 = enumerator2.Current; + + var areEqual = comparer != null + ? comparer.Equals(value1, value2) + : AssertEqualityComparer.Equals(value1, value2); + if (!areEqual) + { + return false; + } + } + + return true; + } + + public static string GetAssertMessage( + IEnumerable expected, + IEnumerable actual, + IEqualityComparer comparer = null, + string prefix = null) + { + Func itemInspector = typeof(T) == typeof(byte) + ? b => $"0x{b:X2}" + : new Func(obj => (obj != null) ? obj.ToString() : ""); + + var itemSeparator = typeof(T) == typeof(byte) + ? ", " + : "," + Environment.NewLine; + + var expectedString = string.Join(itemSeparator, expected.Take(10).Select(itemInspector)); + var actualString = string.Join(itemSeparator, actual.Select(itemInspector)); + + var message = new StringBuilder(); + + if (!string.IsNullOrEmpty(prefix)) + { + message.AppendLine(prefix); + message.AppendLine(); + } + + message.AppendLine("Expected:"); + message.AppendLine(expectedString); + if (expected.Count() > 10) + { + message.AppendLine("... truncated ..."); + } + + message.AppendLine("Actual:"); + message.AppendLine(actualString); + message.AppendLine("Differences:"); + message.AppendLine(DiffUtil.DiffReport(expected, actual, itemSeparator, comparer, itemInspector)); + + return message.ToString(); + } + + private class AssertEqualityComparer : IEqualityComparer + { + public static readonly IEqualityComparer Instance = new AssertEqualityComparer(); + + private static bool CanBeNull() + { + var type = typeof(T); + return !type.GetTypeInfo().IsValueType || + (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); + } + + public static bool IsNull(T @object) + { + if (!CanBeNull()) + { + return false; + } + + return object.Equals(@object, default(T)); + } + + public static bool Equals(T left, T right) + { + return Instance.Equals(left, right); + } + + bool IEqualityComparer.Equals(T x, T y) + { + if (CanBeNull()) + { + if (object.Equals(x, default(T))) + { + return object.Equals(y, default(T)); + } + + if (object.Equals(y, default(T))) + { + return false; + } + } + + if (x is IEquatable equatable) + { + return equatable.Equals(y); + } + + if (x is IComparable comparableT) + { + return comparableT.CompareTo(y) == 0; + } + + if (x is IComparable comparable) + { + return comparable.CompareTo(y) == 0; + } + + if (x is IEnumerable enumerableX && y is IEnumerable enumerableY) + { + var enumeratorX = enumerableX.GetEnumerator(); + var enumeratorY = enumerableY.GetEnumerator(); + + while (true) + { + bool hasNextX = enumeratorX.MoveNext(); + bool hasNextY = enumeratorY.MoveNext(); + + if (!hasNextX || !hasNextY) + { + return hasNextX == hasNextY; + } + + if (!Equals(enumeratorX.Current, enumeratorY.Current)) + { + return false; + } + } + } + + return object.Equals(x, y); + } + + int IEqualityComparer.GetHashCode(T obj) + { + throw new NotImplementedException(); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/DiffUtil.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/DiffUtil.cs new file mode 100644 index 00000000000..f2f38ab78e7 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/DiffUtil.cs @@ -0,0 +1,266 @@ +#nullable disable + +using System.Diagnostics; + +namespace NuGetUpdater.Core.Test.Utilities; + +public class DiffUtil +{ + private enum EditKind + { + /// + /// No change. + /// + None = 0, + + /// + /// Node value was updated. + /// + Update = 1, + + /// + /// Node was inserted. + /// + Insert = 2, + + /// + /// Node was deleted. + /// + Delete = 3, + } + + private class LCS : LongestCommonSubsequence> + { + public static readonly LCS Default = new LCS(EqualityComparer.Default); + + private readonly IEqualityComparer _comparer; + + public LCS(IEqualityComparer comparer) + { + _comparer = comparer; + } + + protected override bool ItemsEqual(IList sequenceA, int indexA, IList sequenceB, int indexB) + { + return _comparer.Equals(sequenceA[indexA], sequenceB[indexB]); + } + + public IEnumerable CalculateDiff(IList sequenceA, IList sequenceB, Func toString) + { + foreach (var edit in GetEdits(sequenceA, sequenceA.Count, sequenceB, sequenceB.Count).Reverse()) + { + switch (edit.Kind) + { + case EditKind.Delete: + yield return "--> " + toString(sequenceA[edit.IndexA]); + break; + + case EditKind.Insert: + yield return "++> " + toString(sequenceB[edit.IndexB]); + break; + + case EditKind.Update: + yield return " " + toString(sequenceB[edit.IndexB]); + break; + } + } + } + } + + public static string DiffReport(IEnumerable expected, IEnumerable actual, string separator, IEqualityComparer comparer = null, Func toString = null) + { + var lcs = (comparer != null) ? new LCS(comparer) : LCS.Default; + toString = toString ?? new Func(obj => obj.ToString()); + + IList expectedList = expected as IList ?? new List(expected); + IList actualList = actual as IList ?? new List(actual); + + return string.Join(separator, lcs.CalculateDiff(expectedList, actualList, toString)); + } + + private static readonly char[] s_lineSplitChars = new[] { '\r', '\n' }; + + public static string[] Lines(string s) + { + return s.Split(s_lineSplitChars, StringSplitOptions.RemoveEmptyEntries); + } + + public static string DiffReport(string expected, string actual) + { + var exlines = Lines(expected); + var aclines = Lines(actual); + return DiffReport(exlines, aclines, separator: Environment.NewLine); + } + + /// + /// Calculates Longest Common Subsequence. + /// + private abstract class LongestCommonSubsequence + { + protected readonly struct Edit + { + public readonly EditKind Kind; + public readonly int IndexA; + public readonly int IndexB; + + internal Edit(EditKind kind, int indexA, int indexB) + { + this.Kind = kind; + this.IndexA = indexA; + this.IndexB = indexB; + } + } + + private const int DeleteCost = 1; + private const int InsertCost = 1; + private const int UpdateCost = 2; + + protected abstract bool ItemsEqual(TSequence sequenceA, int indexA, TSequence sequenceB, int indexB); + + protected IEnumerable> GetMatchingPairs(TSequence sequenceA, int lengthA, TSequence sequenceB, int lengthB) + { + int[,] d = ComputeCostMatrix(sequenceA, lengthA, sequenceB, lengthB); + int i = lengthA; + int j = lengthB; + + while (i != 0 && j != 0) + { + if (d[i, j] == d[i - 1, j] + DeleteCost) + { + i--; + } + else if (d[i, j] == d[i, j - 1] + InsertCost) + { + j--; + } + else + { + i--; + j--; + yield return new KeyValuePair(i, j); + } + } + } + + protected IEnumerable GetEdits(TSequence sequenceA, int lengthA, TSequence sequenceB, int lengthB) + { + int[,] d = ComputeCostMatrix(sequenceA, lengthA, sequenceB, lengthB); + int i = lengthA; + int j = lengthB; + + while (i != 0 && j != 0) + { + if (d[i, j] == d[i - 1, j] + DeleteCost) + { + i--; + yield return new Edit(EditKind.Delete, i, -1); + } + else if (d[i, j] == d[i, j - 1] + InsertCost) + { + j--; + yield return new Edit(EditKind.Insert, -1, j); + } + else + { + i--; + j--; + yield return new Edit(EditKind.Update, i, j); + } + } + + while (i > 0) + { + i--; + yield return new Edit(EditKind.Delete, i, -1); + } + + while (j > 0) + { + j--; + yield return new Edit(EditKind.Insert, -1, j); + } + } + + /// + /// Returns a distance [0..1] of the specified sequences. + /// The smaller distance the more of their elements match. + /// + /// + /// Returns a distance [0..1] of the specified sequences. + /// The smaller distance the more of their elements match. + /// + protected double ComputeDistance(TSequence sequenceA, int lengthA, TSequence sequenceB, int lengthB) + { + Debug.Assert(lengthA >= 0 && lengthB >= 0); + + if (lengthA == 0 || lengthB == 0) + { + return (lengthA == lengthB) ? 0.0 : 1.0; + } + + int lcsLength = 0; + foreach (var pair in GetMatchingPairs(sequenceA, lengthA, sequenceB, lengthB)) + { + lcsLength++; + } + + int max = Math.Max(lengthA, lengthB); + Debug.Assert(lcsLength <= max); + return 1.0 - (double)lcsLength / (double)max; + } + + /// + /// Calculates costs of all paths in an edit graph starting from vertex (0,0) and ending in vertex (lengthA, lengthB). + /// + /// + /// The edit graph for A and B has a vertex at each point in the grid (i,j), i in [0, lengthA] and j in [0, lengthB]. + /// + /// The vertices of the edit graph are connected by horizontal, vertical, and diagonal directed edges to form a directed acyclic graph. + /// Horizontal edges connect each vertex to its right neighbor. + /// Vertical edges connect each vertex to the neighbor below it. + /// Diagonal edges connect vertex (i,j) to vertex (i-1,j-1) if (sequenceA[i-1],sequenceB[j-1]) is true. + /// + /// Editing starts with S = []. + /// Move along horizontal edge (i-1,j)-(i,j) represents the fact that sequenceA[i-1] is not added to S. + /// Move along vertical edge (i,j-1)-(i,j) represents an insert of sequenceB[j-1] to S. + /// Move along diagonal edge (i-1,j-1)-(i,j) represents an addition of sequenceB[j-1] to S via an acceptable + /// change of sequenceA[i-1] to sequenceB[j-1]. + /// + /// In every vertex the cheapest outgoing edge is selected. + /// The number of diagonal edges on the path from (0,0) to (lengthA, lengthB) is the length of the longest common subsequence. + /// + private int[,] ComputeCostMatrix(TSequence sequenceA, int lengthA, TSequence sequenceB, int lengthB) + { + var la = lengthA + 1; + var lb = lengthB + 1; + + // TODO: Optimization possible: O(ND) time, O(N) space + // EUGENE W. MYERS: An O(ND) Difference Algorithm and Its Variations + var d = new int[la, lb]; + + d[0, 0] = 0; + for (int i = 1; i <= lengthA; i++) + { + d[i, 0] = d[i - 1, 0] + DeleteCost; + } + + for (int j = 1; j <= lengthB; j++) + { + d[0, j] = d[0, j - 1] + InsertCost; + } + + for (int i = 1; i <= lengthA; i++) + { + for (int j = 1; j <= lengthB; j++) + { + int m1 = d[i - 1, j - 1] + (ItemsEqual(sequenceA, i - 1, sequenceB, j - 1) ? 0 : UpdateCost); + int m2 = d[i - 1, j] + DeleteCost; + int m3 = d[i, j - 1] + InsertCost; + d[i, j] = Math.Min(Math.Min(m1, m2), m3); + } + } + + return d; + } + } +} From 2fa3d702515ef0a0d1c0f3beaabc680c6c16d6ca Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 11 Mar 2024 23:03:46 -0700 Subject: [PATCH 05/26] Add NuGetUpdater Discover command This command will return the information needed to replace the ruby file_parser. It also provides additional information which can inform how the update_checker and file_updater. In particular it returns the set of TFMs each project uses and dependecy information per project. --- nuget/helpers/lib/NuGet.Client | 2 +- .../EntryPointTests.Discover.cs | 199 +++++++++++ .../Commands/DiscoverCommand.cs | 33 ++ .../NuGetUpdater/NuGetUpdater.Cli/Program.cs | 11 +- .../Discover/DiscoveryWorkerTestBase.cs | 103 ++++++ .../Discover/DiscoveryWorkerTests.cs | 321 ++++++++++++++++++ .../Discover/ExpectedDiscoveryResults.cs | 27 ++ .../TemporaryDirectory.cs | 14 +- .../Utilities/MSBuildHelperTests.cs | 219 ++++++------ .../Utilities/SdkPackageUpdaterHelperTests.cs | 6 +- .../NuGetUpdater.Core/Dependency.cs | 10 +- .../DirectoryPackagesPropsDiscovery.cs | 40 +++ .../DirectoryPackagesPropsDiscoveryResult.cs | 10 + .../Discover/DiscoveryWorker.cs | 206 +++++++++++ .../Discover/DotNetToolsJsonDiscovery.cs | 33 ++ .../DotNetToolsJsonDiscoveryResult.cs | 9 + .../Discover/GlobalJsonDiscovery.cs | 33 ++ .../Discover/GlobalJsonDiscoveryResult.cs | 9 + .../Discover/IDiscoveryResult.cs | 13 + .../Discover/PackagesConfigDiscovery.cs | 33 ++ .../Discover/PackagesConfigDiscoveryResult.cs | 9 + .../Discover/ProjectDiscoveryResult.cs | 12 + .../Discover/SdkProjectDiscovery.cs | 68 ++++ .../Discover/WorkspaceDiscoveryResult.cs | 22 ++ .../Files/ProjectBuildFile.cs | 29 +- .../Updater/DotNetToolsJsonUpdater.cs | 6 +- .../Updater/GlobalJsonUpdater.cs | 2 +- .../Updater/SdkPackageUpdater.cs | 8 +- .../Updater/UpdaterWorker.cs | 12 +- .../Utilities/MSBuildHelper.cs | 54 ++- .../Utilities/NuGetHelper.cs | 6 +- 31 files changed, 1391 insertions(+), 168 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs diff --git a/nuget/helpers/lib/NuGet.Client b/nuget/helpers/lib/NuGet.Client index 993ad4ee235..623fde83a3c 160000 --- a/nuget/helpers/lib/NuGet.Client +++ b/nuget/helpers/lib/NuGet.Client @@ -1 +1 @@ -Subproject commit 993ad4ee2350e33c7821d6609e0099971b534a6a +Subproject commit 623fde83a3cd73cb479ec7fa03866c6116894dbf diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs new file mode 100644 index 00000000000..76d0a58d9a2 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -0,0 +1,199 @@ +using System.Collections.Immutable; +using System.Text; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.Discover; +using NuGetUpdater.Core.Test.Discover; + +using Xunit; + +namespace NuGetUpdater.Cli.Test; + +using TestFile = (string Path, string Content); + +public partial class EntryPointTests +{ + public class Discover : DiscoveryWorkerTestBase + { + [Fact] + public async Task WithSolution() + { + string solutionPath = "path/to/solution.sln"; + await RunAsync(path => + [ + "discover", + "--repo-root", + path, + "--solution-or-project", + Path.Combine(path, solutionPath), + ], + new[] + { + (solutionPath, """ + Microsoft Visual Studio Solution File, Format Version 12.00 + # Visual Studio 14 + VisualStudioVersion = 14.0.22705.0 + MinimumVisualStudioVersion = 10.0.40219.1 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "my", "my.csproj", "{782E0C0A-10D3-444D-9640-263D03D2B20C}" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {782E0C0A-10D3-444D-9640-263D03D2B20C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {782E0C0A-10D3-444D-9640-263D03D2B20C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {782E0C0A-10D3-444D-9640-263D03D2B20C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {782E0C0A-10D3-444D-9640-263D03D2B20C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + """), + ("path/to/my.csproj", """ + + + + v4.5 + + + + + + + packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + """), + ("path/to/packages.config", """ + + + + """) + }, + expectedResult: new() + { + FilePath = solutionPath, + Type = WorkspaceType.Solution, + TargetFrameworks = ["net45"], + Projects = [ + new() + { + FilePath = "path/to/my.csproj", + TargetFrameworks = ["net45"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? + Dependencies = [ + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) + ], + Properties = new Dictionary() + { + ["TargetFrameworkVersion"] = "v4.5", + }.ToImmutableDictionary() + } + ] + }); + } + + [Fact] + public async Task WithProject() + { + var projectPath = "path/to/my.csproj"; + await RunAsync(path => + [ + "discover", + "--repo-root", + path, + "--solution-or-project", + Path.Combine(path, projectPath), + ], + new[] + { + (projectPath, """ + + + + v4.5 + + + + + + + packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + """), + ("path/to/packages.config", """ + + + + """) + }, + expectedResult: new() + { + FilePath = projectPath, + Type = WorkspaceType.Project, + TargetFrameworks = ["net45"], + Projects = [ + new() + { + FilePath = projectPath, + TargetFrameworks = ["net45"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? + Dependencies = [ + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) + ], + Properties = new Dictionary() + { + ["TargetFrameworkVersion"] = "v4.5", + }.ToImmutableDictionary() + } + ] + }); + } + + private static async Task RunAsync( + Func getArgs, + TestFile[] initialFiles, + ExpectedWorkspaceDiscoveryResult expectedResult) + { + var actualResult = await RunDiscoveryAsync(initialFiles, async path => + { + var sb = new StringBuilder(); + var writer = new StringWriter(sb); + + var originalOut = Console.Out; + var originalErr = Console.Error; + Console.SetOut(writer); + Console.SetError(writer); + + try + { + var args = getArgs(path); + var result = await Program.Main(args); + if (result != 0) + { + throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); + } + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); + } + }); + + ValidateWorkspaceResult(expectedResult, actualResult); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs new file mode 100644 index 00000000000..3b28431bee1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs @@ -0,0 +1,33 @@ +using System.CommandLine; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.Discover; + +namespace NuGetUpdater.Cli.Commands; + +internal static class DiscoverCommand +{ + internal static readonly Option RepoRootOption = new("--repo-root", () => new DirectoryInfo(Environment.CurrentDirectory)) { IsRequired = false }; + internal static readonly Option SolutionOrProjectFileOption = new("--solution-or-project") { IsRequired = true }; + internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); + + internal static Command GetCommand(Action setExitCode) + { + Command command = new("discover", "Generates a report of the workspace depenedencies and where they are located.") + { + RepoRootOption, + SolutionOrProjectFileOption, + VerboseOption + }; + + command.TreatUnmatchedTokensAsErrors = true; + + command.SetHandler(async (repoRoot, solutionOrProjectFile, verbose) => + { + var worker = new DiscoveryWorker(new Logger(verbose)); + await worker.RunAsync(repoRoot.FullName, solutionOrProjectFile.FullName); + }, RepoRootOption, SolutionOrProjectFileOption, VerboseOption); + + return command; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs index 6549831eba9..772fb9bf683 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs @@ -1,6 +1,4 @@ -using System; using System.CommandLine; -using System.Threading.Tasks; using NuGetUpdater.Cli.Commands; @@ -16,16 +14,15 @@ internal static async Task Main(string[] args) var command = new RootCommand { FrameworkCheckCommand.GetCommand(setExitCode), + DiscoverCommand.GetCommand(setExitCode), UpdateCommand.GetCommand(setExitCode), }; command.TreatUnmatchedTokensAsErrors = true; var result = await command.InvokeAsync(args); - if (result != 0) - { - exitCode = result; - } - return exitCode; + return result == 0 + ? exitCode + : result; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs new file mode 100644 index 00000000000..00cedb6c723 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -0,0 +1,103 @@ +using System.Collections.Immutable; +using System.Text.Json; + +using NuGetUpdater.Core.Discover; +using NuGetUpdater.Core.Test.Utilities; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Discover; + +using TestFile = (string Path, string Content); + +public class DiscoveryWorkerTestBase +{ + protected static async Task TestDiscovery( + string workspacePath, + TestFile[] files, + ExpectedWorkspaceDiscoveryResult expectedResult) + { + var actualResult = await RunDiscoveryAsync(files, async directoryPath => + { + var worker = new DiscoveryWorker(new Logger(verbose: true)); + await worker.RunAsync(directoryPath, workspacePath); + }); + + ValidateWorkspaceResult(expectedResult, actualResult); + } + + protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult expectedResult, WorkspaceDiscoveryResult actualResult) + { + Assert.NotNull(actualResult); + Assert.Equal(expectedResult.FilePath, actualResult.FilePath); + Assert.Equal(expectedResult.Type, actualResult.Type); + AssertEx.Equal(expectedResult.TargetFrameworks, actualResult.TargetFrameworks); + ValidateDirectoryPackagesProps(expectedResult.DirectoryPackagesProps, actualResult.DirectoryPackagesProps); + ValidateResultWithDependencies(expectedResult.GlobalJson, actualResult.GlobalJson); + ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson); + ValidateProjectResults(expectedResult.Projects, actualResult.Projects); + Assert.Equal(expectedResult.ExpectedProjectCount ?? expectedResult.Projects.Length, actualResult.Projects.Length); + + return; + + void ValidateResultWithDependencies(IDiscoveryResultWithDependencies? expectedResult, IDiscoveryResultWithDependencies? actualResult) + { + if (expectedResult is null) + { + Assert.Null(actualResult); + return; + } + else + { + Assert.NotNull(actualResult); + } + + Assert.Equal(expectedResult.FilePath, actualResult.FilePath); + ValidateDependencies(expectedResult.Dependencies, actualResult.Dependencies); + } + + void ValidateProjectResults(ImmutableArray expectedProjects, ImmutableArray actualProjects) + { + foreach (var expectedProject in expectedProjects) + { + var actualProject = actualProjects.Single(p => p.FilePath == expectedProject.FilePath); + + Assert.Equal(expectedProject.FilePath, actualProject.FilePath); + AssertEx.Equal(expectedProject.Properties, actualProject.Properties); + AssertEx.Equal(expectedProject.TargetFrameworks, actualProject.TargetFrameworks); + AssertEx.Equal(expectedProject.ReferencedProjectPaths, actualProject.ReferencedProjectPaths); + ValidateDependencies(expectedProject.Dependencies, actualProject.Dependencies); + Assert.Equal(expectedProject.ExpectedDependencyCount ?? expectedProject.Dependencies.Length, actualProject.Dependencies.Length); + } + } + + void ValidateDirectoryPackagesProps(DirectoryPackagesPropsDiscoveryResult? expected, DirectoryPackagesPropsDiscoveryResult? actual) + { + ValidateResultWithDependencies(expected, actual); + Assert.Equal(expected?.IsTransitivePinningEnabled, actual?.IsTransitivePinningEnabled); + } + + void ValidateDependencies(ImmutableArray expectedDependencies, ImmutableArray actualDependencies) + { + foreach (var expectedDependency in expectedDependencies) + { + var actualDependency = actualDependencies.Single(d => d.Name == expectedDependency.Name); + Assert.Equal(expectedDependency, actualDependency); + } + } + } + + protected static async Task RunDiscoveryAsync(TestFile[] files, Func action) + { + // write initial files + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files); + + // run discovery + await action(temporaryDirectory.DirectoryPath); + + // gather results + var resultPath = Path.Join(temporaryDirectory.DirectoryPath, DiscoveryWorker.DiscoveryResultFileName); + var resultJson = await File.ReadAllTextAsync(resultPath); + return JsonSerializer.Deserialize(resultJson, DiscoveryWorker.SerializerOptions)!; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs new file mode 100644 index 00000000000..412703cef11 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -0,0 +1,321 @@ +using System.Collections.Immutable; + +using NuGetUpdater.Core.Discover; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Discover; + +public class DiscoveryWorkerTests : DiscoveryWorkerTestBase +{ + [Theory] + [InlineData("src/project.csproj")] + [InlineData("src/project.vbproj")] + [InlineData("src/project.fsproj")] + public async Task TestProjectFiles(string projectPath) + { + await TestDiscovery( + workspacePath: projectPath, + files: new[] + { + (projectPath, """ + + + netstandard2.0 + 9.0.1 + + + + + + + """) + }, + expectedResult: new() + { + FilePath = projectPath, + Type = WorkspaceType.Project, + TargetFrameworks = ["netstandard2.0"], + Projects = [ + new() + { + FilePath = projectPath, + TargetFrameworks = ["netstandard2.0"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 18, + Dependencies = [ + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) + ], + Properties = new Dictionary() + { + ["NewtonsoftJsonPackageVersion"] = "9.0.1", + ["TargetFramework"] = "netstandard2.0", + }.ToImmutableDictionary() + } + ] + } + ); + } + + [Fact] + public async Task TestPackageConfig() + { + var projectPath = "src/project.csproj"; + await TestDiscovery( + workspacePath: projectPath, + files: new[] + { + (projectPath, """ + + + + v4.5 + + + + + + + packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + """), + ("src/packages.config", """ + + + + """), + }, + expectedResult: new() + { + FilePath = projectPath, + Type = WorkspaceType.Project, + TargetFrameworks = ["net45"], + Projects = [ + new() + { + FilePath = projectPath, + TargetFrameworks = ["net45"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? + Dependencies = [ + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) + ], + Properties = new Dictionary() + { + ["TargetFrameworkVersion"] = "v4.5", + }.ToImmutableDictionary() + } + ] + } + ); + } + + [Fact] + public async Task TestProps() + { + var projectPath = "src/project.csproj"; + await TestDiscovery( + workspacePath: projectPath, + files: new[] + { + (projectPath, """ + + + netstandard2.0 + + + + + + + """), + ("Directory.Packages.props", """ + + + true + 9.0.1 + + + + + + + """) + }, + expectedResult: new() + { + FilePath = projectPath, + Type = WorkspaceType.Project, + TargetFrameworks = ["netstandard2.0"], + ExpectedProjectCount = 2, + Projects = [ + new() + { + FilePath = projectPath, + TargetFrameworks = ["netstandard2.0"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 18, + Dependencies = [ + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) + ], + Properties = new Dictionary() + { + ["ManagePackageVersionsCentrally"] = "true", + ["NewtonsoftJsonPackageVersion"] = "9.0.1", + ["TargetFramework"] = "netstandard2.0", + }.ToImmutableDictionary() + } + ], + DirectoryPackagesProps = new() + { + FilePath = "Directory.Packages.props", + Dependencies = [ + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageVersion, IsDirect: true) + ], + } + } + ); + } + + [Fact] + public async Task TestRepo() + { + var solutionPath = "solution.sln"; + await TestDiscovery( + workspacePath: solutionPath, + files: new[] + { + ("src/project.csproj", """ + + + netstandard2.0 + + + + + + + """), + ("Directory.Packages.props", """ + + + true + 9.0.1 + + + + + + + """), + (solutionPath, """ + Microsoft Visual Studio Solution File, Format Version 12.00 + # Visual Studio 14 + VisualStudioVersion = 14.0.22705.0 + MinimumVisualStudioVersion = 10.0.40219.1 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "project", ".\src\project.csproj", "{782E0C0A-10D3-444D-9640-263D03D2B20C}" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {782E0C0A-10D3-444D-9640-263D03D2B20C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {782E0C0A-10D3-444D-9640-263D03D2B20C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {782E0C0A-10D3-444D-9640-263D03D2B20C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {782E0C0A-10D3-444D-9640-263D03D2B20C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + """), + ("global.json", """ + { + "sdk": { + "version": "6.0.405", + "rollForward": "latestPatch" + }, + "msbuild-sdks": { + "My.Custom.Sdk": "5.0.0", + "My.Other.Sdk": "1.0.0-beta" + } + } + """), + (".config/dotnet-tools.json", """ + { + "version": 1, + "isRoot": true, + "tools": { + "microsoft.botsay": { + "version": "1.0.0", + "commands": [ + "botsay" + ] + }, + "dotnetsay": { + "version": "2.1.3", + "commands": [ + "dotnetsay" + ] + } + } + } + """), + }, + expectedResult: new() + { + FilePath = solutionPath, + Type = WorkspaceType.Solution, + TargetFrameworks = ["netstandard2.0"], + ExpectedProjectCount = 2, + Projects = [ + new() + { + FilePath = "src/project.csproj", + TargetFrameworks = ["netstandard2.0"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 18, + Dependencies = [ + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) + ], + Properties = new Dictionary() + { + ["ManagePackageVersionsCentrally"] = "true", + ["NewtonsoftJsonPackageVersion"] = "9.0.1", + ["TargetFramework"] = "netstandard2.0", + }.ToImmutableDictionary() + } + ], + DirectoryPackagesProps = new() + { + FilePath = "Directory.Packages.props", + Dependencies = [ + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageVersion, IsDirect: true) + ], + }, + GlobalJson = new() + { + FilePath = "global.json", + Dependencies = [ + new("My.Custom.Sdk", "5.0.0", DependencyType.MSBuildSdk), + new("My.Other.Sdk", "1.0.0-beta", DependencyType.MSBuildSdk), + ] + }, + DotNetToolsJson = new() + { + FilePath = ".config/dotnet-tools.json", + Dependencies = [ + new("microsoft.botsay", "1.0.0", DependencyType.DotNetTool), + new("dotnetsay", "2.1.3", DependencyType.DotNetTool), + ] + } + } + ); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs new file mode 100644 index 00000000000..5ad8879a778 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs @@ -0,0 +1,27 @@ +using System.Collections.Immutable; + +using NuGetUpdater.Core.Discover; + +namespace NuGetUpdater.Core.Test.Discover; + +public record ExpectedWorkspaceDiscoveryResult : IDiscoveryResult +{ + public required string FilePath { get; init; } + public WorkspaceType Type { get; init; } + public ImmutableArray TargetFrameworks { get; init; } + public ImmutableArray Projects { get; init; } + public int? ExpectedProjectCount { get; init; } + public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } + public GlobalJsonDiscoveryResult? GlobalJson { get; init; } + public DotNetToolsJsonDiscoveryResult? DotNetToolsJson { get; init; } +} + +public record ExpectedSdkProjectDiscoveryResult : IDiscoveryResultWithDependencies +{ + public required string FilePath { get; init; } + public required ImmutableDictionary Properties { get; init; } + public ImmutableArray TargetFrameworks { get; init; } + public ImmutableArray ReferencedProjectPaths { get; init; } + public ImmutableArray Dependencies { get; init; } + public int? ExpectedDependencyCount { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs index 3c74ce79596..0b7ee407f75 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; +namespace NuGetUpdater.Core.Test; using TestFile = (string Path, string Contents); -namespace NuGetUpdater.Core.Test; - public sealed class TemporaryDirectory : IDisposable { public string DirectoryPath { get; } @@ -52,6 +47,13 @@ public static async Task CreateWithContentsAsync(params Test { var temporaryDirectory = new TemporaryDirectory(); + var parentDirectory = Path.GetDirectoryName(temporaryDirectory.DirectoryPath)!; + + // prevent directory crawling + await File.WriteAllTextAsync(Path.Combine(parentDirectory, "Directory.Build.props"), ""); + await File.WriteAllTextAsync(Path.Combine(parentDirectory, "Directory.Build.targets"), ""); + await File.WriteAllTextAsync(Path.Combine(parentDirectory, "Directory.Packages.props"), ""); + foreach (var (path, contents) in fileContents) { var localPath = path.StartsWith('/') ? path[1..] : path; // remove path rooting character diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs index 3469f733c2f..a6f0a035404 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Xunit; @@ -100,7 +95,7 @@ public void ProjectPathsCanBeParsedFromSolutionFiles(string solutionContent, str expectedPaths = expectedPaths.Select(p => p.Replace("/", "\\")).ToArray(); } - Assert.Equal(expectedPaths, actualProjectSubPaths); + AssertEx.Equal(expectedPaths, actualProjectSubPaths); } finally { @@ -122,7 +117,7 @@ public void TfmsCanBeDeterminedFromProjectContents(string projectContents, strin var expectedTfms = new[] { expectedTfm1, expectedTfm2 }.Where(tfm => tfm is not null).ToArray(); var buildFile = ProjectBuildFile.Open(Path.GetDirectoryName(projectPath)!, projectPath); var actualTfms = MSBuildHelper.GetTargetFrameworkMonikers(ImmutableArray.Create(buildFile)); - Assert.Equal(expectedTfms, actualTfms); + AssertEx.Equal(expectedTfms, actualTfms); } finally { @@ -144,7 +139,7 @@ public async Task TopLevelPackageDependenciesCanBeDetermined((string Path, strin } var actualTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles.ToImmutableArray()); - Assert.Equal(expectedTopLevelDependencies, actualTopLevelDependencies); + AssertEx.Equal(expectedTopLevelDependencies, actualTopLevelDependencies); } [Fact] @@ -153,29 +148,29 @@ public async Task AllPackageDependenciesCanBeTraversed() using var temp = new TemporaryDirectory(); var expectedDependencies = new Dependency[] { - new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown), + new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown), - new("System.Buffers", "4.5.1", DependencyType.Unknown), - new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown), - new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown), - new("System.Memory", "4.5.5", DependencyType.Unknown), - new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown), - new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown), - new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown), + new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Buffers", "4.5.1", DependencyType.Unknown, IsTransitive: true), + new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Memory", "4.5.5", DependencyType.Unknown, IsTransitive: true), + new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, IsTransitive: true), + new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, IsTransitive: true), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), }; var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( temp.DirectoryPath, temp.DirectoryPath, "netstandard2.0", [new Dependency("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown)]); - Assert.Equal(expectedDependencies, actualDependencies); + AssertEx.Equal(expectedDependencies, actualDependencies); } [Fact] @@ -191,72 +186,72 @@ public async Task AllPackageDependencies_DoNotTruncateLongDependencyLists() new("Microsoft.ApplicationInsights.PerfCounterCollector", "2.10.0", DependencyType.Unknown), new("Microsoft.ApplicationInsights.WindowsServer", "2.10.0", DependencyType.Unknown), new("Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel", "2.10.0", DependencyType.Unknown), - new("Microsoft.AspNet.TelemetryCorrelation", "1.0.5", DependencyType.Unknown), - new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Caching.Abstractions", "1.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Caching.Memory", "1.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.DiagnosticAdapter", "1.1.0", DependencyType.Unknown), + new("Microsoft.AspNet.TelemetryCorrelation", "1.0.5", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Caching.Abstractions", "1.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Caching.Memory", "1.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.DiagnosticAdapter", "1.1.0", DependencyType.Unknown, IsTransitive: true), new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.PlatformAbstractions", "1.1.0", DependencyType.Unknown), - new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown), + new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.PlatformAbstractions", "1.1.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, IsTransitive: true), new("Moq", "4.16.1", DependencyType.Unknown), new("MSTest.TestFramework", "2.1.0", DependencyType.Unknown), new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown), new("System", "4.1.311.2", DependencyType.Unknown), - new("System.Buffers", "4.5.1", DependencyType.Unknown), - new("System.Collections.Concurrent", "4.3.0", DependencyType.Unknown), - new("System.Collections.Immutable", "1.3.0", DependencyType.Unknown), - new("System.Collections.NonGeneric", "4.3.0", DependencyType.Unknown), - new("System.Collections.Specialized", "4.3.0", DependencyType.Unknown), - new("System.ComponentModel", "4.3.0", DependencyType.Unknown), - new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown), - new("System.ComponentModel.Primitives", "4.3.0", DependencyType.Unknown), - new("System.ComponentModel.TypeConverter", "4.3.0", DependencyType.Unknown), + new("System.Buffers", "4.5.1", DependencyType.Unknown, IsTransitive: true), + new("System.Collections.Concurrent", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Collections.Immutable", "1.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Collections.NonGeneric", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Collections.Specialized", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.ComponentModel", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.ComponentModel.Primitives", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.ComponentModel.TypeConverter", "4.3.0", DependencyType.Unknown, IsTransitive: true), new("System.Core", "3.5.21022.801", DependencyType.Unknown), - new("System.Data.Common", "4.3.0", DependencyType.Unknown), - new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown), - new("System.Diagnostics.PerformanceCounter", "4.5.0", DependencyType.Unknown), - new("System.Diagnostics.StackTrace", "4.3.0", DependencyType.Unknown), - new("System.Dynamic.Runtime", "4.3.0", DependencyType.Unknown), - new("System.IO.FileSystem.Primitives", "4.3.0", DependencyType.Unknown), - new("System.Linq", "4.3.0", DependencyType.Unknown), - new("System.Linq.Expressions", "4.3.0", DependencyType.Unknown), - new("System.Memory", "4.5.5", DependencyType.Unknown), - new("System.Net.WebHeaderCollection", "4.3.0", DependencyType.Unknown), - new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown), - new("System.ObjectModel", "4.3.0", DependencyType.Unknown), - new("System.Private.DataContractSerialization", "4.3.0", DependencyType.Unknown), - new("System.Reflection.Emit", "4.3.0", DependencyType.Unknown), - new("System.Reflection.Emit.ILGeneration", "4.3.0", DependencyType.Unknown), - new("System.Reflection.Emit.Lightweight", "4.3.0", DependencyType.Unknown), - new("System.Reflection.Metadata", "1.4.1", DependencyType.Unknown), - new("System.Reflection.TypeExtensions", "4.3.0", DependencyType.Unknown), - new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown), - new("System.Runtime.InteropServices.RuntimeInformation", "4.3.0", DependencyType.Unknown), - new("System.Runtime.Numerics", "4.3.0", DependencyType.Unknown), - new("System.Runtime.Serialization.Json", "4.3.0", DependencyType.Unknown), - new("System.Runtime.Serialization.Primitives", "4.3.0", DependencyType.Unknown), - new("System.Security.Claims", "4.3.0", DependencyType.Unknown), - new("System.Security.Cryptography.OpenSsl", "4.3.0", DependencyType.Unknown), - new("System.Security.Cryptography.Primitives", "4.3.0", DependencyType.Unknown), - new("System.Security.Principal", "4.3.0", DependencyType.Unknown), - new("System.Text.RegularExpressions", "4.3.0", DependencyType.Unknown), - new("System.Threading", "4.3.0", DependencyType.Unknown), - new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown), - new("System.Threading.Thread", "4.3.0", DependencyType.Unknown), - new("System.Threading.ThreadPool", "4.3.0", DependencyType.Unknown), - new("System.Xml.ReaderWriter", "4.3.0", DependencyType.Unknown), - new("System.Xml.XDocument", "4.3.0", DependencyType.Unknown), - new("System.Xml.XmlDocument", "4.3.0", DependencyType.Unknown), - new("System.Xml.XmlSerializer", "4.3.0", DependencyType.Unknown), + new("System.Data.Common", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Diagnostics.PerformanceCounter", "4.5.0", DependencyType.Unknown, IsTransitive: true), + new("System.Diagnostics.StackTrace", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Dynamic.Runtime", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.IO.FileSystem.Primitives", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Linq", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Linq.Expressions", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Memory", "4.5.5", DependencyType.Unknown, IsTransitive: true), + new("System.Net.WebHeaderCollection", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, IsTransitive: true), + new("System.ObjectModel", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Private.DataContractSerialization", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Reflection.Emit", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Reflection.Emit.ILGeneration", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Reflection.Emit.Lightweight", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Reflection.Metadata", "1.4.1", DependencyType.Unknown, IsTransitive: true), + new("System.Reflection.TypeExtensions", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Runtime.InteropServices.RuntimeInformation", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Runtime.Numerics", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Runtime.Serialization.Json", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Runtime.Serialization.Primitives", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Security.Claims", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Security.Cryptography.OpenSsl", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Security.Cryptography.Primitives", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Security.Principal", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Text.RegularExpressions", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Threading", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, IsTransitive: true), + new("System.Threading.Thread", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Threading.ThreadPool", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Xml.ReaderWriter", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Xml.XDocument", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Xml.XmlDocument", "4.3.0", DependencyType.Unknown, IsTransitive: true), + new("System.Xml.XmlSerializer", "4.3.0", DependencyType.Unknown, IsTransitive: true), new("Microsoft.ApplicationInsights.Web", "2.10.0", DependencyType.Unknown), new("MSTest.TestAdapter", "2.1.0", DependencyType.Unknown), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), }; var packages = new[] { @@ -284,7 +279,7 @@ public async Task AllPackageDependencies_DoNotTruncateLongDependencyLists() Assert.Equal(ed, ad); } - Assert.Equal(expectedDependencies, actualDependencies); + AssertEx.Equal(expectedDependencies, actualDependencies); } [Fact] @@ -293,22 +288,22 @@ public async Task AllPackageDependencies_DoNotIncludeUpdateOnlyPackages() using var temp = new TemporaryDirectory(); var expectedDependencies = new Dependency[] { - new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown), + new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown), - new("System.Buffers", "4.5.1", DependencyType.Unknown), - new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown), - new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown), - new("System.Memory", "4.5.5", DependencyType.Unknown), - new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown), - new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown), - new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown), + new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Buffers", "4.5.1", DependencyType.Unknown, IsTransitive: true), + new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Memory", "4.5.5", DependencyType.Unknown, IsTransitive: true), + new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, IsTransitive: true), + new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, IsTransitive: true), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), }; var packages = new[] { @@ -316,7 +311,7 @@ public async Task AllPackageDependencies_DoNotIncludeUpdateOnlyPackages() new Dependency("Newtonsoft.Json", "12.0.1", DependencyType.Unknown, IsUpdate: true) }; var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(temp.DirectoryPath, temp.DirectoryPath, "netstandard2.0", packages); - Assert.Equal(expectedDependencies, actualDependencies); + AssertEx.Equal(expectedDependencies, actualDependencies); } [Fact] @@ -410,7 +405,7 @@ await File.WriteAllTextAsync( var expectedDependencies = new Dependency[] { new("Newtonsoft.Json", "4.5.11", DependencyType.Unknown), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), }; var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( temp.DirectoryPath, @@ -419,7 +414,7 @@ await File.WriteAllTextAsync( [new Dependency("Newtonsoft.Json", "4.5.11", DependencyType.Unknown)] ); Assert.False(Directory.Exists(tempNuGetHttpCacheDirectory), "The .nuget/.v3-cache directory was created, meaning http was used."); - Assert.Equal(expectedDependencies, actualDependencies); + AssertEx.Equal(expectedDependencies, actualDependencies); } finally { @@ -469,16 +464,16 @@ await File.WriteAllTextAsync( var expectedDependencies = new Dependency[] { new("Microsoft.CodeAnalysis.Common", "4.8.0-3.23457.5", DependencyType.Unknown), - new("System.Buffers", "4.5.1", DependencyType.Unknown), - new("System.Collections.Immutable", "7.0.0", DependencyType.Unknown), - new("System.Memory", "4.5.5", DependencyType.Unknown), - new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown), - new("System.Reflection.Metadata", "7.0.0", DependencyType.Unknown), - new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown), - new("System.Text.Encoding.CodePages", "7.0.0", DependencyType.Unknown), - new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown), - new("Microsoft.CodeAnalysis.Analyzers", "3.3.4", DependencyType.Unknown), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown), + new("System.Buffers", "4.5.1", DependencyType.Unknown, IsTransitive: true), + new("System.Collections.Immutable", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Memory", "4.5.5", DependencyType.Unknown, IsTransitive: true), + new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, IsTransitive: true), + new("System.Reflection.Metadata", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Text.Encoding.CodePages", "7.0.0", DependencyType.Unknown, IsTransitive: true), + new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.CodeAnalysis.Analyzers", "3.3.4", DependencyType.Unknown, IsTransitive: true), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), }; var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( temp.DirectoryPath, @@ -486,7 +481,7 @@ await File.WriteAllTextAsync( "netstandard2.0", [new Dependency("Microsoft.CodeAnalysis.Common", "4.8.0-3.23457.5", DependencyType.Unknown)] ); - Assert.Equal(expectedDependencies, actualDependencies); + AssertEx.Equal(expectedDependencies, actualDependencies); } finally { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs index 6c735c737c4..b21ad5cd43b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs @@ -1,7 +1,3 @@ -using System.IO; -using System.Linq; -using System.Threading.Tasks; - using Xunit; namespace NuGetUpdater.Core.Test.Utilities @@ -171,7 +167,7 @@ public async Task BuildFileEnumerationWithGlobalJsonWithComments() private static async Task LoadBuildFilesFromTemp(TemporaryDirectory temporaryDirectory, string relativeProjectPath) { - var buildFiles = await MSBuildHelper.LoadBuildFiles(temporaryDirectory.DirectoryPath, $"{temporaryDirectory.DirectoryPath}/{relativeProjectPath}"); + var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(temporaryDirectory.DirectoryPath, $"{temporaryDirectory.DirectoryPath}/{relativeProjectPath}"); var buildFilePaths = buildFiles.Select(f => f.RepoRelativePath.NormalizePathToUnix()).ToArray(); return buildFilePaths; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs index 203f8e71a33..756ba0c1db4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs @@ -1,3 +1,11 @@ namespace NuGetUpdater.Core; -public sealed record Dependency(string Name, string? Version, DependencyType Type, bool IsDevDependency = false, bool IsOverride = false, bool IsUpdate = false); +public sealed record Dependency( + string Name, + string? Version, + DependencyType Type, + bool IsDevDependency = false, + bool IsDirect = false, + bool IsTransitive = false, + bool IsOverride = false, + bool IsUpdate = false); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs new file mode 100644 index 00000000000..a268f2e8fb4 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs @@ -0,0 +1,40 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +internal static class DirectoryPackagesPropsDiscovery +{ + public static DirectoryPackagesPropsDiscoveryResult? Discover(string repoRootPath, string workspacePath, ImmutableArray projectResults, Logger logger) + { + var projectResult = projectResults.FirstOrDefault(p => p.Properties.TryGetValue("ManagePackageVersionsCentrally", out var value) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)); + if (projectResult is null) + { + return null; + } + + var projectFilePath = Path.GetFullPath(projectResult.FilePath, repoRootPath); + if (MSBuildHelper.GetDirectoryPackagesPropsPath(repoRootPath, workspacePath) is not { } directoryPackagesPropsPath) + { + logger.Log(" No Directory.Packages.props file found."); + return null; + } + + var relativeDirectoryPackagesPropsPath = Path.GetRelativePath(repoRootPath, directoryPackagesPropsPath); + var directoryPackagesPropsFile = projectResults.FirstOrDefault(p => p.FilePath == relativeDirectoryPackagesPropsPath); + if (directoryPackagesPropsFile is null) + { + logger.Log($" No project file found for [{relativeDirectoryPackagesPropsPath}]."); + return null; + } + + logger.Log($" Discovered [{directoryPackagesPropsFile.FilePath}] file."); + + var isTransitivePinningEnabled = projectResult.Properties.TryGetValue("EnableTransitivePinning", out var value) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + return new() + { + FilePath = directoryPackagesPropsFile.FilePath, + IsTransitivePinningEnabled = isTransitivePinningEnabled, + Dependencies = directoryPackagesPropsFile.Dependencies, + }; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs new file mode 100644 index 00000000000..c93a0234431 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +public sealed record DirectoryPackagesPropsDiscoveryResult : IDiscoveryResultWithDependencies +{ + public required string FilePath { get; init; } + public bool IsTransitivePinningEnabled { get; init; } + public ImmutableArray Dependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs new file mode 100644 index 00000000000..f0cbd77c044 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -0,0 +1,206 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NuGetUpdater.Core.Discover; + +public partial class DiscoveryWorker +{ + public const string DiscoveryResultFileName = ".dependabot/discovery.json"; + + private readonly Logger _logger; + private readonly HashSet _processedProjectPaths = new(StringComparer.OrdinalIgnoreCase); + + internal static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() }, + }; + + public DiscoveryWorker(Logger logger) + { + _logger = logger; + } + + public async Task RunAsync(string repoRootPath, string workspacePath) + { + MSBuildHelper.RegisterMSBuild(); + + if (!Path.IsPathRooted(workspacePath) || !File.Exists(workspacePath)) + { + workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath)); + } + + var dotNetToolsJsonDiscovery = DotNetToolsJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); + var globalJsonDiscovery = GlobalJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); + + WorkspaceType workspaceType = WorkspaceType.Unknown; + ImmutableArray projectResults = []; + + var extension = Path.GetExtension(workspacePath).ToLowerInvariant(); + switch (extension) + { + case ".sln": + workspaceType = WorkspaceType.Solution; + projectResults = await RunForSolutionAsync(repoRootPath, workspacePath); + break; + case ".proj": + workspaceType = WorkspaceType.DirsProj; + projectResults = await RunForProjFileAsync(repoRootPath, workspacePath); + break; + case ".csproj": + case ".fsproj": + case ".vbproj": + workspaceType = WorkspaceType.Project; + projectResults = await RunForProjectAsync(repoRootPath, workspacePath); + break; + default: + _logger.Log($"File extension [{extension}] is not supported."); + break; + } + + var directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger); + + var result = new WorkspaceDiscoveryResult + { + FilePath = Path.GetRelativePath(repoRootPath, workspacePath), + TargetFrameworks = projectResults + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .ToImmutableArray(), + Type = workspaceType, + DotNetToolsJson = dotNetToolsJsonDiscovery, + GlobalJson = globalJsonDiscovery, + DirectoryPackagesProps = directoryPackagesPropsDiscovery, + Projects = projectResults, + }; + + await WriteResults(repoRootPath, result); + + _logger.Log("Discovery complete."); + + _processedProjectPaths.Clear(); + } + + private async Task> RunForSolutionAsync(string repoRootPath, string solutionPath) + { + _logger.Log($"Running for solution [{Path.GetRelativePath(repoRootPath, solutionPath)}]"); + if (!File.Exists(solutionPath)) + { + _logger.Log($"File [{solutionPath}] does not exist."); + return []; + } + + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + var projectPaths = MSBuildHelper.GetProjectPathsFromSolution(solutionPath); + foreach (var projectPath in projectPaths) + { + var projectResults = await RunForProjectAsync(repoRootPath, projectPath); + foreach (var projectResult in projectResults) + { + if (results.ContainsKey(projectResult.FilePath)) + { + continue; + } + + results[projectResult.FilePath] = projectResult; + } + } + + return [.. results.Values]; + } + + private async Task> RunForProjFileAsync(string repoRootPath, string projFilePath) + { + _logger.Log($"Running for proj file [{Path.GetRelativePath(repoRootPath, projFilePath)}]"); + if (!File.Exists(projFilePath)) + { + _logger.Log($"File [{projFilePath}] does not exist."); + return []; + } + + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + var projectPaths = MSBuildHelper.GetProjectPathsFromProject(projFilePath); + foreach (var projectPath in projectPaths) + { + // If there is some MSBuild logic that needs to run to fully resolve the path skip the project + if (File.Exists(projectPath)) + { + var projectResults = await RunForProjectAsync(repoRootPath, projectPath); + foreach (var projectResult in projectResults) + { + if (results.ContainsKey(projectResult.FilePath)) + { + continue; + } + + results[projectResult.FilePath] = projectResult; + } + } + } + + return [.. results.Values]; + } + + private async Task> RunForProjectAsync(string repoRootPath, string projectFilePath) + { + var relativeProjectPath = Path.GetRelativePath(repoRootPath, projectFilePath); + _logger.Log($"Running for project file [{relativeProjectPath}]"); + if (!File.Exists(projectFilePath)) + { + _logger.Log($"File [{projectFilePath}] does not exist."); + return []; + } + + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + var projectPaths = MSBuildHelper.GetProjectPathsFromProject(projectFilePath); + foreach (var projectPath in projectPaths.Prepend(projectFilePath)) + { + // If there is some MSBuild logic that needs to run to fully resolve the path skip the project + if (!File.Exists(projectPath)) + { + continue; + } + + var packagesConfigDependencies = PackagesConfigDiscovery.Discover(repoRootPath, projectPath, _logger) + ?.Dependencies; + + var projectResults = await SdkProjectDiscovery.DiscoverAsync(repoRootPath, projectPath, _logger); + foreach (var projectResult in projectResults) + { + if (results.ContainsKey(projectResult.FilePath)) + { + continue; + } + + // If we had packages.config dependencies, merge them with the project dependencies + if (projectResult.FilePath == relativeProjectPath && packagesConfigDependencies is not null) + { + results[projectResult.FilePath] = projectResult with + { + Dependencies = [.. projectResult.Dependencies, .. packagesConfigDependencies], + }; + } + else + { + results[projectResult.FilePath] = projectResult; + } + } + } + + return [.. results.Values]; + } + + private static async Task WriteResults(string repoRootPath, WorkspaceDiscoveryResult result) + { + var resultPath = Path.GetFullPath(DiscoveryResultFileName, repoRootPath); + var resultDirectory = Path.GetDirectoryName(resultPath)!; + if (!Directory.Exists(resultDirectory)) + { + Directory.CreateDirectory(resultDirectory); + } + + var resultJson = JsonSerializer.Serialize(result, SerializerOptions); + await File.WriteAllTextAsync(path: resultPath, resultJson); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs new file mode 100644 index 00000000000..dfe72917ee6 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs @@ -0,0 +1,33 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +internal static class DotNetToolsJsonDiscovery +{ + public static DotNetToolsJsonDiscoveryResult? Discover(string repoRootPath, string workspacePath, Logger logger) + { + var dotnetToolsJsonFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); + if (dotnetToolsJsonFile is null) + { + logger.Log(" No dotnet-tools.json file found."); + return null; + } + + logger.Log($" Discovered [{dotnetToolsJsonFile.RepoRelativePath}] file."); + + var dependencies = BuildFile.GetDependencies(dotnetToolsJsonFile); + + return new() + { + FilePath = dotnetToolsJsonFile.RepoRelativePath, + Dependencies = dependencies.ToImmutableArray(), + }; + } + + private static DotNetToolsJsonBuildFile? TryLoadBuildFile(string repoRootPath, string workspacePath, Logger logger) + { + return MSBuildHelper.GetDotNetToolsJsonPath(repoRootPath, workspacePath) is { } dotnetToolsJsonPath + ? DotNetToolsJsonBuildFile.Open(repoRootPath, dotnetToolsJsonPath, logger) + : null; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs new file mode 100644 index 00000000000..e3247ee8207 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +public sealed record DotNetToolsJsonDiscoveryResult : IDiscoveryResultWithDependencies +{ + public required string FilePath { get; init; } + public ImmutableArray Dependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs new file mode 100644 index 00000000000..414b9bd7645 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs @@ -0,0 +1,33 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +internal static class GlobalJsonDiscovery +{ + public static GlobalJsonDiscoveryResult? Discover(string repoRootPath, string workspacePath, Logger logger) + { + var globalJsonFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); + if (globalJsonFile is null) + { + logger.Log(" No global.json file found."); + return null; + } + + logger.Log($" Discovered [{globalJsonFile.RepoRelativePath}] file."); + + var dependencies = BuildFile.GetDependencies(globalJsonFile); + + return new() + { + FilePath = globalJsonFile.RepoRelativePath, + Dependencies = dependencies.ToImmutableArray(), + }; + } + + private static GlobalJsonBuildFile? TryLoadBuildFile(string repoRootPath, string workspacePath, Logger logger) + { + return MSBuildHelper.GetGlobalJsonPath(repoRootPath, workspacePath) is { } globalJsonPath + ? GlobalJsonBuildFile.Open(repoRootPath, globalJsonPath, logger) + : null; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs new file mode 100644 index 00000000000..186c4133a9b --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +public sealed record GlobalJsonDiscoveryResult : IDiscoveryResultWithDependencies +{ + public required string FilePath { get; init; } + public ImmutableArray Dependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs new file mode 100644 index 00000000000..45f58eb659d --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs @@ -0,0 +1,13 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +public interface IDiscoveryResult +{ + string FilePath { get; } +} + +public interface IDiscoveryResultWithDependencies : IDiscoveryResult +{ + ImmutableArray Dependencies { get; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs new file mode 100644 index 00000000000..b31eda825b7 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs @@ -0,0 +1,33 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +internal static class PackagesConfigDiscovery +{ + public static PackagesConfigDiscoveryResult? Discover(string repoRootPath, string workspacePath, Logger logger) + { + var packagesConfigFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); + if (packagesConfigFile is null) + { + logger.Log(" No packages.config file found."); + return null; + } + + logger.Log($" Discovered [{packagesConfigFile.RepoRelativePath}] file."); + + var dependencies = BuildFile.GetDependencies(packagesConfigFile); + + return new() + { + FilePath = packagesConfigFile.RepoRelativePath, + Dependencies = dependencies.ToImmutableArray(), + }; + } + + private static PackagesConfigBuildFile? TryLoadBuildFile(string repoRootPath, string projectPath, Logger logger) + { + return NuGetHelper.HasPackagesConfigFile(projectPath, out var packagesConfigPath) + ? PackagesConfigBuildFile.Open(repoRootPath, packagesConfigPath) + : null; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs new file mode 100644 index 00000000000..1f278f3f97e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +public sealed record PackagesConfigDiscoveryResult : IDiscoveryResultWithDependencies +{ + public required string FilePath { get; init; } + public ImmutableArray Dependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs new file mode 100644 index 00000000000..7162da4948a --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +public record ProjectDiscoveryResult : IDiscoveryResultWithDependencies +{ + public required string FilePath { get; init; } + public required ImmutableDictionary Properties { get; init; } + public ImmutableArray TargetFrameworks { get; init; } = []; + public ImmutableArray ReferencedProjectPaths { get; init; } = []; + public required ImmutableArray Dependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs new file mode 100644 index 00000000000..f903a357c93 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs @@ -0,0 +1,68 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +internal static class SdkProjectDiscovery +{ + public static async Task> DiscoverAsync(string repoRootPath, string projectPath, Logger logger) + { + // Determine which targets and props files contribute to the build. + var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath); + + // Get all the dependencies which are directly referenced from the project file or indirectly referenced from + // targets and props files. + var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles); + + var results = ImmutableArray.CreateBuilder(); + foreach (var buildFile in buildFiles) + { + // The build file dependencies have the correct DependencyType and the TopLevelDependencies have the evaluated version. + // Combine them to have the set of dependencies that are directly referenced from the build file. + var fileDependencies = BuildFile.GetDependencies(buildFile) + .ToDictionary(d => d.Name, StringComparer.OrdinalIgnoreCase); + var directDependencies = topLevelDependencies + .Where(d => fileDependencies.ContainsKey(d.Name)) + .Select(d => + { + var dependency = fileDependencies[d.Name]; + return d with + { + Type = dependency.Type, + IsDirect = true + }; + }).ToImmutableArray(); + + if (buildFile.GetFileType() == ProjectBuildFileType.Project) + { + // Collect information that is specific to the project file. + var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles).ToImmutableArray(); + var properties = MSBuildHelper.GetProperties(buildFiles).ToImmutableDictionary(); + var referencedProjectPaths = MSBuildHelper.GetProjectPathsFromProject(projectPath).ToImmutableArray(); + + // Get the complete set of dependencies including transitive dependencies. + var allDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(repoRootPath, projectPath, tfms.First(), directDependencies, logger); + var dependencies = directDependencies.Concat(allDependencies.Where(d => d.IsTransitive)).ToImmutableArray(); + + results.Add(new() + { + FilePath = buildFile.RepoRelativePath, + Properties = properties, + TargetFrameworks = tfms, + ReferencedProjectPaths = referencedProjectPaths, + Dependencies = dependencies, + }); + } + else + { + results.Add(new() + { + FilePath = buildFile.RepoRelativePath, + Properties = ImmutableDictionary.Empty, + Dependencies = directDependencies, + }); + } + } + + return results.ToImmutable(); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs new file mode 100644 index 00000000000..9b37498e2c2 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs @@ -0,0 +1,22 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Discover; + +public sealed record WorkspaceDiscoveryResult : IDiscoveryResult +{ + public required string FilePath { get; init; } + public WorkspaceType Type { get; init; } + public ImmutableArray TargetFrameworks { get; init; } + public ImmutableArray Projects { get; init; } + public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } + public GlobalJsonDiscoveryResult? GlobalJson { get; init; } + public DotNetToolsJsonDiscoveryResult? DotNetToolsJson { get; init; } +} + +public enum WorkspaceType +{ + Unknown, + Solution, + DirsProj, + Project, +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs index cb4e79df0d0..99f24254aa8 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core; @@ -92,4 +87,28 @@ public void NormalizeDirectorySeparatorsInProject() (_, n) => n.WithContent(n.GetContentValue().Replace("/", "\\")).AsNode); Update(updatedXml); } + + public ProjectBuildFileType GetFileType() + { + var extension = System.IO.Path.GetExtension(Path); + return extension.ToLower() switch + { + ".csproj" => ProjectBuildFileType.Project, + ".vbproj" => ProjectBuildFileType.Project, + ".fsproj" => ProjectBuildFileType.Project, + ".proj" => ProjectBuildFileType.Proj, + ".props" => ProjectBuildFileType.Props, + ".targets" => ProjectBuildFileType.Targets, + _ => ProjectBuildFileType.Unknown + }; + } +} + +public enum ProjectBuildFileType +{ + Unknown, + Proj, + Project, + Props, + Targets } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs index bf838aab415..cd1a9ed31b6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs @@ -1,7 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - namespace NuGetUpdater.Core; internal static class DotNetToolsJsonUpdater @@ -12,7 +8,7 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string works var dotnetToolsJsonFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); if (dotnetToolsJsonFile is null) { - logger.Log(" No dotnet-tools.json files found."); + logger.Log(" No dotnet-tools.json file found."); return; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs index 7a6b1b74962..7488c9f061d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs @@ -17,7 +17,7 @@ public static async Task UpdateDependencyAsync( var globalJsonFile = LoadBuildFile(repoRootPath, workspacePath, logger); if (globalJsonFile is null) { - logger.Log(" No global.json files found."); + logger.Log(" No global.json file found."); return; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 6f7f21e7635..6d264303a7e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -25,7 +25,7 @@ public static async Task UpdateDependencyAsync( // SDK-style project, modify the XML directly logger.Log(" Running for SDK-style project"); - var buildFiles = await MSBuildHelper.LoadBuildFiles(repoRootPath, projectPath); + var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath); var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles); // Get the set of all top-level dependencies in the current project @@ -84,7 +84,7 @@ private static async Task DoesDependencyRequireUpdateAsync( tfm, topLevelDependencies, logger); - foreach (var (packageName, packageVersion, _, _, _, _) in dependencies) + foreach (var (packageName, packageVersion, _, _, _, _, _, _) in dependencies) { if (packageVersion is null) { @@ -231,7 +231,7 @@ private static async Task AddTransitiveDependencyAsync(string projectPath, strin logger.Log($" Adding [{dependencyName}/{newDependencyVersion}] as a top-level package reference."); // see https://learn.microsoft.com/nuget/consume-packages/install-use-packages-dotnet-cli - var (exitCode, _, _) = await ProcessEx.RunAsync("dotnet", $"add {projectPath} package {dependencyName} --version {newDependencyVersion}"); + var (exitCode, output, error) = await ProcessEx.RunAsync("dotnet", $"add {projectPath} package {dependencyName} --version {newDependencyVersion}"); if (exitCode != 0) { logger.Log($" Transitive dependency [{dependencyName}/{newDependencyVersion}] was not added."); @@ -269,7 +269,7 @@ private static async Task AddTransitiveDependencyAsync(string projectPath, strin var packagesAndVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var (_, dependencies) in tfmsAndDependencies) { - foreach (var (packageName, packageVersion, _, _, _, _) in dependencies) + foreach (var (packageName, packageVersion, _, _, _, _, _, _) in dependencies) { if (packagesAndVersions.TryGetValue(packageName, out var existingVersion) && existingVersion != packageVersion) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs index 0259aacc6d2..3b75334d5ae 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - namespace NuGetUpdater.Core; public class UpdaterWorker @@ -50,6 +44,8 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string dep break; } + _logger.Log("Update complete."); + _processedProjectPaths.Clear(); } @@ -138,14 +134,12 @@ private async Task RunUpdaterAsync( _logger.Log($"Updating project [{projectPath}]"); - if (NuGetHelper.HasPackagesConfigFile(projectPath)) + if (NuGetHelper.HasPackagesConfigFile(projectPath, out _)) { await PackagesConfigUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, _logger); } // Some repos use a mix of packages.config and PackageReference await SdkPackageUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, _logger); - - _logger.Log("Update complete."); } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index f911a791e36..dc6e94b0a0b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; -using System.Threading.Tasks; using System.Xml; using Microsoft.Build.Construction; @@ -170,6 +165,32 @@ public static IEnumerable GetProjectPathsFromProject(string projFilePath } } + public static IReadOnlyDictionary GetProperties(ImmutableArray buildFiles) + { + Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); + + foreach (var buildFile in buildFiles) + { + var projectRoot = CreateProjectRootElement(buildFile); + + foreach (var property in projectRoot.Properties) + { + // Short of evaluating the entire project, there's no way to _really_ know what package version is + // going to be used, and even then we might not be able to update it. As a best guess, we'll simply + // skip any property that has a condition _or_ where the condition is checking for an empty string. + var hasEmptyCondition = string.IsNullOrEmpty(property.Condition); + var conditionIsCheckingForEmptyString = string.Equals(property.Condition, $"$({property.Name}) == ''", StringComparison.OrdinalIgnoreCase) || + string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase); + if (hasEmptyCondition || conditionIsCheckingForEmptyString) + { + propertyInfo[property.Name] = property.Value; + } + } + } + + return propertyInfo; + } + public static IEnumerable GetTopLevelPackageDependencyInfos(ImmutableArray buildFiles) { Dictionary packageInfo = new(StringComparer.OrdinalIgnoreCase); @@ -260,7 +281,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl /// /// Given an MSBuild string and a set of properties, returns our best guess at the final value MSBuild will evaluate to. /// - public static EvaluationResult GetEvaluatedValue(string msbuildString, Dictionary propertyInfo, params string[] propertiesToIgnore) + public static EvaluationResult GetEvaluatedValue(string msbuildString, IReadOnlyDictionary propertyInfo, params string[] propertiesToIgnore) { var ignoredProperties = new HashSet(propertiesToIgnore, StringComparer.OrdinalIgnoreCase); var seenProperties = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -444,11 +465,16 @@ await File.WriteAllTextAsync( } internal static async Task GetAllPackageDependenciesAsync( - string repoRoot, string projectPath, string targetFramework, IReadOnlyCollection packages, Logger? logger = null) + string repoRoot, + string projectPath, + string targetFramework, + IReadOnlyCollection packages, + Logger? logger = null) { var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_"); try { + var topLevelPackagesNames = packages.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase); var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages); var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\" /t:_ReportDependencies"); @@ -460,7 +486,12 @@ internal static async Task GetAllPackageDependenciesAsync( var allDependencies = lines .Select(line => pattern.Match(line)) .Where(match => match.Success) - .Select(match => new Dependency(match.Groups["PackageName"].Value, match.Groups["PackageVersion"].Value, DependencyType.Unknown)) + .Select(match => + { + var packageName = match.Groups["PackageName"].Value; + var isTransitive = !topLevelPackagesNames.Contains(packageName); + return new Dependency(packageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, IsTransitive: isTransitive); + }) .ToArray(); return allDependencies; @@ -493,7 +524,12 @@ internal static async Task GetAllPackageDependenciesAsync( return PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./.config/dotnet-tools.json"); } - internal static async Task> LoadBuildFiles(string repoRootPath, string projectPath) + internal static string? GetDirectoryPackagesPropsPath(string repoRootPath, string workspacePath) + { + return PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./Directory.Packages.props"); + } + + internal static async Task> LoadBuildFilesAsync(string repoRootPath, string projectPath) { var buildFileList = new List { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs index dd3e568edf6..06abdca17f3 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.Diagnostics.CodeAnalysis; namespace NuGetUpdater.Core; @@ -6,10 +6,10 @@ internal static class NuGetHelper { internal const string PackagesConfigFileName = "packages.config"; - public static bool HasPackagesConfigFile(string projectPath) + public static bool HasPackagesConfigFile(string projectPath, [NotNullWhen(returnValue: true)] out string? packagesConfigPath) { var projectDirectory = Path.GetDirectoryName(projectPath); - var packagesConfigPath = PathHelper.JoinPath(projectDirectory, PackagesConfigFileName); + packagesConfigPath = PathHelper.JoinPath(projectDirectory, PackagesConfigFileName); return File.Exists(packagesConfigPath); } } From 762b8ed5398cc164d4c47cf7c0dce5eda11f44e6 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Tue, 12 Mar 2024 08:07:34 -0700 Subject: [PATCH 06/26] Return additional information about properties and evaluted dependency versions --- .../EntryPointTests.Discover.cs | 8 +- .../Discover/DiscoveryWorkerTestBase.cs | 6 +- .../Discover/DiscoveryWorkerTests.cs | 26 +++--- .../Discover/ExpectedDiscoveryResults.cs | 2 +- .../Files/PackagesConfigBuildFileTests.cs | 2 +- .../Utilities/MSBuildHelperTests.cs | 90 ++++++++++++++----- .../NuGetUpdater.Core/Dependency.cs | 1 + .../DirectoryPackagesPropsDiscovery.cs | 4 +- .../Discover/ProjectDiscoveryResult.cs | 2 +- .../Discover/SdkProjectDiscovery.cs | 2 +- .../NuGetUpdater.Core/EvaluationResult.cs | 9 ++ .../NuGetUpdater.Core/EvaluationResultType.cs | 9 ++ .../Files/PackagesConfigBuildFile.cs | 2 +- .../NuGetUpdater.Core/Property.cs | 6 ++ .../Updater/SdkPackageUpdater.cs | 4 +- .../Utilities/MSBuildHelper.cs | 52 +++++------ 16 files changed, 149 insertions(+), 76 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResultType.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Property.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index 76d0a58d9a2..094b0164127 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -91,9 +91,9 @@ await RunAsync(path => Dependencies = [ new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) ], - Properties = new Dictionary() + Properties = new Dictionary() { - ["TargetFrameworkVersion"] = "v4.5", + ["TargetFrameworkVersion"] = new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), }.ToImmutableDictionary() } ] @@ -153,9 +153,9 @@ await RunAsync(path => Dependencies = [ new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) ], - Properties = new Dictionary() + Properties = new Dictionary() { - ["TargetFrameworkVersion"] = "v4.5", + ["TargetFrameworkVersion"] = new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), }.ToImmutableDictionary() } ] diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index 00cedb6c723..48d62c9dcb7 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -82,7 +82,11 @@ void ValidateDependencies(ImmutableArray expectedDependencies, Immut foreach (var expectedDependency in expectedDependencies) { var actualDependency = actualDependencies.Single(d => d.Name == expectedDependency.Name); - Assert.Equal(expectedDependency, actualDependency); + Assert.Equal(expectedDependency.Name, actualDependency.Name); + Assert.Equal(expectedDependency.Version, actualDependency.Version); + Assert.Equal(expectedDependency.Type, actualDependency.Type); + Assert.Equal(expectedDependency.IsDirect, actualDependency.IsDirect); + Assert.Equal(expectedDependency.IsTransitive, actualDependency.IsTransitive); } } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs index 412703cef11..1dc11229369 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -46,10 +46,10 @@ await TestDiscovery( Dependencies = [ new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) ], - Properties = new Dictionary() + Properties = new Dictionary() { - ["NewtonsoftJsonPackageVersion"] = "9.0.1", - ["TargetFramework"] = "netstandard2.0", + ["NewtonsoftJsonPackageVersion"] = new("NewtonsoftJsonPackageVersion", "9.0.1", projectPath), + ["TargetFramework"] = new("TargetFramework", "netstandard2.0", projectPath), }.ToImmutableDictionary() } ] @@ -104,9 +104,9 @@ await TestDiscovery( Dependencies = [ new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) ], - Properties = new Dictionary() + Properties = new Dictionary() { - ["TargetFrameworkVersion"] = "v4.5", + ["TargetFrameworkVersion"] = new("TargetFrameworkVersion", "v4.5", projectPath), }.ToImmutableDictionary() } ] @@ -162,11 +162,11 @@ await TestDiscovery( Dependencies = [ new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) ], - Properties = new Dictionary() + Properties = new Dictionary() { - ["ManagePackageVersionsCentrally"] = "true", - ["NewtonsoftJsonPackageVersion"] = "9.0.1", - ["TargetFramework"] = "netstandard2.0", + ["ManagePackageVersionsCentrally"] = new("ManagePackageVersionsCentrally", "true", "Directory.Packages.props"), + ["NewtonsoftJsonPackageVersion"] = new("NewtonsoftJsonPackageVersion", "9.0.1", "Directory.Packages.props"), + ["TargetFramework"] = new("TargetFramework", "netstandard2.0", projectPath), }.ToImmutableDictionary() } ], @@ -284,11 +284,11 @@ await TestDiscovery( Dependencies = [ new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) ], - Properties = new Dictionary() + Properties = new Dictionary() { - ["ManagePackageVersionsCentrally"] = "true", - ["NewtonsoftJsonPackageVersion"] = "9.0.1", - ["TargetFramework"] = "netstandard2.0", + ["ManagePackageVersionsCentrally"] = new("ManagePackageVersionsCentrally", "true", "Directory.Packages.props"), + ["NewtonsoftJsonPackageVersion"] = new("NewtonsoftJsonPackageVersion", "9.0.1", "Directory.Packages.props"), + ["TargetFramework"] = new("TargetFramework", "netstandard2.0", "src/project.csproj"), }.ToImmutableDictionary() } ], diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs index 5ad8879a778..dbff5b9d3b5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs @@ -19,7 +19,7 @@ public record ExpectedWorkspaceDiscoveryResult : IDiscoveryResult public record ExpectedSdkProjectDiscoveryResult : IDiscoveryResultWithDependencies { public required string FilePath { get; init; } - public required ImmutableDictionary Properties { get; init; } + public required ImmutableDictionary Properties { get; init; } public ImmutableArray TargetFrameworks { get; init; } public ImmutableArray ReferencedProjectPaths { get; init; } public ImmutableArray Dependencies { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs index c1156506be1..19b5cea7d81 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs @@ -38,7 +38,7 @@ public void PackagesConfig_GetDependencies_ReturnsDependencies() var expectedDependencies = new List { new("Microsoft.CodeDom.Providers.DotNetCompilerPlatform", "1.0.0", DependencyType.PackageConfig), - new("Microsoft.Net.Compilers", "1.0.0", DependencyType.PackageConfig, true), + new("Microsoft.Net.Compilers", "1.0.0", DependencyType.PackageConfig, IsDevDependency: true), new("Newtonsoft.Json", "8.0.3", DependencyType.PackageConfig) }; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs index a6f0a035404..c23fb922b30 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -4,6 +4,8 @@ namespace NuGetUpdater.Core.Test.Utilities; +using TestFile = (string Path, string Content); + public class MSBuildHelperTests { public MSBuildHelperTests() @@ -25,15 +27,15 @@ public void GetRootedValue_FindsValue() """; - var propertyInfo = new Dictionary + var propertyInfo = new Dictionary { - { "PackageVersion1", "1.1.1" }, + { "PackageVersion1", new("PackageVersion1", "1.1.1", "Packages.props") }, }; // Act - var (resultType, evaluatedValue, _) = MSBuildHelper.GetEvaluatedValue(projectContents, propertyInfo); + var (resultType, _, evaluatedValue, _, _, _) = MSBuildHelper.GetEvaluatedValue(projectContents, propertyInfo); - Assert.Equal(MSBuildHelper.EvaluationResultType.Success, resultType); + Assert.Equal(EvaluationResultType.Success, resultType); // Assert Assert.Equal(""" @@ -62,19 +64,19 @@ public async Task GetRootedValue_DoesNotRecurseAsync() """; - var propertyInfo = new Dictionary + var propertyInfo = new Dictionary { - { "PackageVersion1", "$(PackageVersion2)" }, - { "PackageVersion2", "$(PackageVersion1)" } + { "PackageVersion1", new("PackageVersion1", "$(PackageVersion2)", "Packages.props") }, + { "PackageVersion2", new("PackageVersion2", "$(PackageVersion1)", "Packages.props") } }; // This is needed to make the timeout work. Without that we could get caugth in an infinite loop. await Task.Delay(1); // Act - var (resultType, _, errorMessage) = MSBuildHelper.GetEvaluatedValue(projectContents, propertyInfo); + var (resultType, _, _, _, _, errorMessage) = MSBuildHelper.GetEvaluatedValue(projectContents, propertyInfo); // Assert - Assert.Equal(MSBuildHelper.EvaluationResultType.CircularReference, resultType); + Assert.Equal(EvaluationResultType.CircularReference, resultType); Assert.Equal("Property 'PackageVersion1' has a circular reference.", errorMessage); } @@ -127,7 +129,7 @@ public void TfmsCanBeDeterminedFromProjectContents(string projectContents, strin [Theory] [MemberData(nameof(GetTopLevelPackageDependencyInfosTestData))] - public async Task TopLevelPackageDependenciesCanBeDetermined((string Path, string Content)[] buildFileContents, Dependency[] expectedTopLevelDependencies) + public async Task TopLevelPackageDependenciesCanBeDetermined(TestFile[] buildFileContents, Dependency[] expectedTopLevelDependencies) { using var testDirectory = new TemporaryDirectory(); var buildFiles = new List(); @@ -510,7 +512,11 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) + new( + "Newtonsoft.Json", + "12.0.1", + DependencyType.Unknown, + EvaluationResult: new(EvaluationResultType.Success, "12.0.1", "12.0.1", null, null, null)) } ]; @@ -533,7 +539,11 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) + new( + "Newtonsoft.Json", + "12.0.1", + DependencyType.Unknown, + EvaluationResult: new(EvaluationResultType.Success, "12.0.1", "12.0.1", null, null, null)) } ]; @@ -557,7 +567,11 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) + new( + "Newtonsoft.Json", + "12.0.1", + DependencyType.Unknown, + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) } ]; @@ -583,7 +597,11 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) + new( + "Newtonsoft.Json", + "12.0.1", + DependencyType.Unknown, + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) } ]; @@ -609,7 +627,11 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) + new( + "Newtonsoft.Json", + "12.0.1", + DependencyType.Unknown, + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) } }; @@ -635,7 +657,11 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) + new( + "Newtonsoft.Json", + "12.0.1", + DependencyType.Unknown, + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) } ]; @@ -661,7 +687,11 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) + new( + "Newtonsoft.Json", + "12.0.1", + DependencyType.Unknown, + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) } }; @@ -693,8 +723,17 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Azure.Identity", "1.6.0", DependencyType.Unknown), - new("Microsoft.Data.SqlClient", "5.1.4", DependencyType.Unknown, IsUpdate: true) + new( + "Azure.Identity", + "1.6.0", + DependencyType.Unknown, + EvaluationResult: new(EvaluationResultType.Success, "1.6.0", "1.6.0", null, null, null)), + new( + "Microsoft.Data.SqlClient", + "5.1.4", + DependencyType.Unknown, + EvaluationResult: new(EvaluationResultType.Success, "5.1.4", "5.1.4", null, null, null), + IsUpdate: true), } ]; @@ -726,8 +765,17 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() // expected dependencies new Dependency[] { - new("Azure.Identity", "1.6.0", DependencyType.Unknown), - new("Microsoft.Data.SqlClient", "5.1.4", DependencyType.Unknown, IsUpdate: true) + new( + "Azure.Identity", + "1.6.0", + DependencyType.Unknown, + EvaluationResult: new(EvaluationResultType.Success, "1.6.0", "1.6.0", null, null, null)), + new( + "Microsoft.Data.SqlClient", + "5.1.4", + DependencyType.Unknown, + EvaluationResult: new(EvaluationResultType.Success, "5.1.4", "5.1.4", null, null, null), + IsUpdate: true), } ]; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs index 756ba0c1db4..0c8f55f957d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs @@ -4,6 +4,7 @@ public sealed record Dependency( string Name, string? Version, DependencyType Type, + EvaluationResult? EvaluationResult = null, bool IsDevDependency = false, bool IsDirect = false, bool IsTransitive = false, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs index a268f2e8fb4..1277ac826d1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs @@ -6,7 +6,7 @@ internal static class DirectoryPackagesPropsDiscovery { public static DirectoryPackagesPropsDiscoveryResult? Discover(string repoRootPath, string workspacePath, ImmutableArray projectResults, Logger logger) { - var projectResult = projectResults.FirstOrDefault(p => p.Properties.TryGetValue("ManagePackageVersionsCentrally", out var value) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)); + var projectResult = projectResults.FirstOrDefault(p => p.Properties.TryGetValue("ManagePackageVersionsCentrally", out var property) && string.Equals(property.Value, "true", StringComparison.OrdinalIgnoreCase)); if (projectResult is null) { return null; @@ -29,7 +29,7 @@ internal static class DirectoryPackagesPropsDiscovery logger.Log($" Discovered [{directoryPackagesPropsFile.FilePath}] file."); - var isTransitivePinningEnabled = projectResult.Properties.TryGetValue("EnableTransitivePinning", out var value) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + var isTransitivePinningEnabled = projectResult.Properties.TryGetValue("EnableTransitivePinning", out var property) && string.Equals(property.Value, "true", StringComparison.OrdinalIgnoreCase); return new() { FilePath = directoryPackagesPropsFile.FilePath, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs index 7162da4948a..15edb4b45bd 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs @@ -5,7 +5,7 @@ namespace NuGetUpdater.Core.Discover; public record ProjectDiscoveryResult : IDiscoveryResultWithDependencies { public required string FilePath { get; init; } - public required ImmutableDictionary Properties { get; init; } + public required ImmutableDictionary Properties { get; init; } public ImmutableArray TargetFrameworks { get; init; } = []; public ImmutableArray ReferencedProjectPaths { get; init; } = []; public required ImmutableArray Dependencies { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs index f903a357c93..c3966ac7c9b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs @@ -57,7 +57,7 @@ public static async Task> DiscoverAsync(s results.Add(new() { FilePath = buildFile.RepoRelativePath, - Properties = ImmutableDictionary.Empty, + Properties = ImmutableDictionary.Empty, Dependencies = directDependencies, }); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs new file mode 100644 index 00000000000..99d6061f54c --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs @@ -0,0 +1,9 @@ +namespace NuGetUpdater.Core; + +public record EvaluationResult( + EvaluationResultType ResultType, + string OriginalValue, + string EvaluatedValue, + string? FirstPropertyName, + string? LastPropertyName, + string? ErrorMessage); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResultType.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResultType.cs new file mode 100644 index 00000000000..45e0384de02 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResultType.cs @@ -0,0 +1,9 @@ +namespace NuGetUpdater.Core; + +public enum EvaluationResultType +{ + Success, + PropertyIgnored, + CircularReference, + PropertyNotFound, +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs index 2f4b892141b..1c7b26222ea 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs @@ -28,5 +28,5 @@ public IEnumerable GetDependencies() => Packages p.GetAttributeValue("id", StringComparison.OrdinalIgnoreCase), p.GetAttributeValue("version", StringComparison.OrdinalIgnoreCase), DependencyType.PackageConfig, - (p.GetAttribute("developmentDependency", StringComparison.OrdinalIgnoreCase)?.Value ?? "false").Equals(true.ToString(), StringComparison.OrdinalIgnoreCase))); + IsDevDependency: (p.GetAttribute("developmentDependency", StringComparison.OrdinalIgnoreCase)?.Value ?? "false").Equals(true.ToString(), StringComparison.OrdinalIgnoreCase))); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Property.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Property.cs new file mode 100644 index 00000000000..70faca4a6c5 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Property.cs @@ -0,0 +1,6 @@ +namespace NuGetUpdater.Core; + +public sealed record Property( + string Name, + string Value, + string SourceFilePath); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 6d264303a7e..3d75b555bb1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -84,7 +84,7 @@ private static async Task DoesDependencyRequireUpdateAsync( tfm, topLevelDependencies, logger); - foreach (var (packageName, packageVersion, _, _, _, _, _, _) in dependencies) + foreach (var (packageName, packageVersion, _, _, _, _, _, _, _) in dependencies) { if (packageVersion is null) { @@ -269,7 +269,7 @@ private static async Task AddTransitiveDependencyAsync(string projectPath, strin var packagesAndVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var (_, dependencies) in tfmsAndDependencies) { - foreach (var (packageName, packageVersion, _, _, _, _, _, _) in dependencies) + foreach (var (packageName, packageVersion, _, _, _, _, _, _, _) in dependencies) { if (packagesAndVersions.TryGetValue(packageName, out var existingVersion) && existingVersion != packageVersion) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index dc6e94b0a0b..40173460eea 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -18,8 +18,6 @@ namespace NuGetUpdater.Core; -using EvaluationResult = (MSBuildHelper.EvaluationResultType ResultType, string EvaluatedValue, string? ErrorMessage); - internal static partial class MSBuildHelper { public static string MSBuildPath { get; private set; } = string.Empty; @@ -45,7 +43,7 @@ public static void RegisterMSBuild() public static string[] GetTargetFrameworkMonikers(ImmutableArray buildFiles) { HashSet targetFrameworkValues = new(StringComparer.OrdinalIgnoreCase); - Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); + Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); foreach (var buildFile in buildFiles) { @@ -68,7 +66,7 @@ public static string[] GetTargetFrameworkMonikers(ImmutableArray GetProjectPathsFromProject(string projFilePath } } - public static IReadOnlyDictionary GetProperties(ImmutableArray buildFiles) + public static IReadOnlyDictionary GetProperties(ImmutableArray buildFiles) { - Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); + Dictionary properties = new(StringComparer.OrdinalIgnoreCase); foreach (var buildFile in buildFiles) { @@ -183,19 +181,19 @@ public static IReadOnlyDictionary GetProperties(ImmutableArray

GetTopLevelPackageDependencyInfos(ImmutableArray buildFiles) { Dictionary packageInfo = new(StringComparer.OrdinalIgnoreCase); Dictionary packageVersionInfo = new(StringComparer.OrdinalIgnoreCase); - Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); + Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); foreach (var buildFile in buildFiles) { @@ -248,7 +246,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase); if (hasEmptyCondition || conditionIsCheckingForEmptyString) { - propertyInfo[property.Name] = property.Value; + propertyInfo[property.Name] = new(property.Name, property.Value, buildFile.RepoRelativePath); } } } @@ -273,40 +271,46 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl // We don't know the version for range requirements or wildcard // requirements, so return "" for these. yield return packageVersion.Contains(',') || packageVersion.Contains('*') - ? new Dependency(name, string.Empty, DependencyType.Unknown, IsUpdate: isUpdate) - : new Dependency(name, packageVersion, DependencyType.Unknown, IsUpdate: isUpdate); + ? new Dependency(name, string.Empty, DependencyType.Unknown, EvaluationResult: evaluationResult, IsUpdate: isUpdate) + : new Dependency(name, packageVersion, DependencyType.Unknown, EvaluationResult: evaluationResult, IsUpdate: isUpdate); } } ///

/// Given an MSBuild string and a set of properties, returns our best guess at the final value MSBuild will evaluate to. /// - public static EvaluationResult GetEvaluatedValue(string msbuildString, IReadOnlyDictionary propertyInfo, params string[] propertiesToIgnore) + public static EvaluationResult GetEvaluatedValue(string msbuildString, IReadOnlyDictionary propertyInfo, params string[] propertiesToIgnore) { var ignoredProperties = new HashSet(propertiesToIgnore, StringComparer.OrdinalIgnoreCase); var seenProperties = new HashSet(StringComparer.OrdinalIgnoreCase); + string originalValue = msbuildString; + string? firstPropertyName = null; + string? lastPropertyName = null; while (TryGetPropertyName(msbuildString, out var propertyName)) { + firstPropertyName ??= propertyName; + lastPropertyName = propertyName; + if (ignoredProperties.Contains(propertyName)) { - return (EvaluationResultType.PropertyIgnored, msbuildString, $"Property '{propertyName}' is ignored."); + return new(EvaluationResultType.PropertyIgnored, originalValue, msbuildString, firstPropertyName, lastPropertyName, $"Property '{propertyName}' is ignored."); } if (!seenProperties.Add(propertyName)) { - return (EvaluationResultType.CircularReference, msbuildString, $"Property '{propertyName}' has a circular reference."); + return new(EvaluationResultType.CircularReference, originalValue, msbuildString, firstPropertyName, lastPropertyName, $"Property '{propertyName}' has a circular reference."); } - if (!propertyInfo.TryGetValue(propertyName, out var propertyValue)) + if (!propertyInfo.TryGetValue(propertyName, out var property)) { - return (EvaluationResultType.PropertyNotFound, msbuildString, $"Property '{propertyName}' was not found."); + return new(EvaluationResultType.PropertyNotFound, originalValue, msbuildString, firstPropertyName, lastPropertyName, $"Property '{propertyName}' was not found."); } - msbuildString = msbuildString.Replace($"$({propertyName})", propertyValue); + msbuildString = msbuildString.Replace($"$({propertyName})", property.Value); } - return (EvaluationResultType.Success, msbuildString, null); + return new(EvaluationResultType.Success, originalValue, msbuildString, firstPropertyName, lastPropertyName, null); } public static bool TryGetPropertyName(string versionContent, [NotNullWhen(true)] out string? propertyName) @@ -599,12 +603,4 @@ internal static async Task> LoadBuildFilesAsync [GeneratedRegex("^\\s*NuGetData::Package=(?[^,]+), Version=(?.+)$")] private static partial Regex PackagePattern(); - - internal enum EvaluationResultType - { - Success, - PropertyIgnored, - CircularReference, - PropertyNotFound, - } } From 6f9e89c618c47306956d6e0c36dda5b8ce12f200 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 13 Mar 2024 08:19:18 -0700 Subject: [PATCH 07/26] Support Discovery from a directory path --- .../EntryPointTests.Discover.cs | 66 ++++++++- .../Commands/DiscoverCommand.cs | 10 +- .../Discover/DiscoveryWorker.cs | 126 +++++++++--------- .../Discover/WorkspaceDiscoveryResult.cs | 1 + 4 files changed, 136 insertions(+), 67 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index 094b0164127..75f4f5b441e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -24,7 +24,7 @@ await RunAsync(path => "discover", "--repo-root", path, - "--solution-or-project", + "--workspace", Path.Combine(path, solutionPath), ], new[] @@ -109,7 +109,7 @@ await RunAsync(path => "discover", "--repo-root", path, - "--solution-or-project", + "--workspace", Path.Combine(path, projectPath), ], new[] @@ -162,6 +162,68 @@ await RunAsync(path => }); } + [Fact] + public async Task WithDirectory() + { + var workspacePath = "path/to/"; + await RunAsync(path => + [ + "discover", + "--repo-root", + path, + "--workspace", + Path.Combine(path, workspacePath), + ], + new[] + { + ("path/to/my.csproj", """ + + + + v4.5 + + + + + + + packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + """), + ("path/to/packages.config", """ + + + + """) + }, + expectedResult: new() + { + FilePath = workspacePath, + Type = WorkspaceType.Directory, + TargetFrameworks = ["net45"], + Projects = [ + new() + { + FilePath = "path/to/my.csproj", + TargetFrameworks = ["net45"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? + Dependencies = [ + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) + ], + Properties = new Dictionary() + { + ["TargetFrameworkVersion"] = new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), + }.ToImmutableDictionary() + } + ] + }); + } + private static async Task RunAsync( Func getArgs, TestFile[] initialFiles, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs index 3b28431bee1..b73b93322fc 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs @@ -8,7 +8,7 @@ namespace NuGetUpdater.Cli.Commands; internal static class DiscoverCommand { internal static readonly Option RepoRootOption = new("--repo-root", () => new DirectoryInfo(Environment.CurrentDirectory)) { IsRequired = false }; - internal static readonly Option SolutionOrProjectFileOption = new("--solution-or-project") { IsRequired = true }; + internal static readonly Option WorkspaceOption = new("--workspace") { IsRequired = true }; internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); internal static Command GetCommand(Action setExitCode) @@ -16,17 +16,17 @@ internal static Command GetCommand(Action setExitCode) Command command = new("discover", "Generates a report of the workspace depenedencies and where they are located.") { RepoRootOption, - SolutionOrProjectFileOption, + WorkspaceOption, VerboseOption }; command.TreatUnmatchedTokensAsErrors = true; - command.SetHandler(async (repoRoot, solutionOrProjectFile, verbose) => + command.SetHandler(async (repoRoot, workspace, verbose) => { var worker = new DiscoveryWorker(new Logger(verbose)); - await worker.RunAsync(repoRoot.FullName, solutionOrProjectFile.FullName); - }, RepoRootOption, SolutionOrProjectFileOption, VerboseOption); + await worker.RunAsync(repoRoot.FullName, workspace.FullName); + }, RepoRootOption, WorkspaceOption, VerboseOption); return command; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index f0cbd77c044..89f55e516b6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -26,7 +26,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath) { MSBuildHelper.RegisterMSBuild(); - if (!Path.IsPathRooted(workspacePath) || !File.Exists(workspacePath)) + if (!Path.IsPathRooted(workspacePath) || (!File.Exists(workspacePath) && !Directory.Exists(workspacePath))) { workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath)); } @@ -37,26 +37,34 @@ public async Task RunAsync(string repoRootPath, string workspacePath) WorkspaceType workspaceType = WorkspaceType.Unknown; ImmutableArray projectResults = []; - var extension = Path.GetExtension(workspacePath).ToLowerInvariant(); - switch (extension) + if (File.Exists(workspacePath)) { - case ".sln": - workspaceType = WorkspaceType.Solution; - projectResults = await RunForSolutionAsync(repoRootPath, workspacePath); - break; - case ".proj": - workspaceType = WorkspaceType.DirsProj; - projectResults = await RunForProjFileAsync(repoRootPath, workspacePath); - break; - case ".csproj": - case ".fsproj": - case ".vbproj": - workspaceType = WorkspaceType.Project; - projectResults = await RunForProjectAsync(repoRootPath, workspacePath); - break; - default: - _logger.Log($"File extension [{extension}] is not supported."); - break; + var extension = Path.GetExtension(workspacePath).ToLowerInvariant(); + switch (extension) + { + case ".sln": + workspaceType = WorkspaceType.Solution; + projectResults = await RunForSolutionAsync(repoRootPath, workspacePath); + break; + case ".proj": + workspaceType = WorkspaceType.DirsProj; + projectResults = await RunForProjFileAsync(repoRootPath, workspacePath); + break; + case ".csproj": + case ".fsproj": + case ".vbproj": + workspaceType = WorkspaceType.Project; + projectResults = await RunForProjectAsync(repoRootPath, workspacePath); + break; + default: + _logger.Log($"File extension [{extension}] is not supported."); + break; + } + } + else + { + workspaceType = WorkspaceType.Directory; + projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath); } var directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger); @@ -82,6 +90,27 @@ public async Task RunAsync(string repoRootPath, string workspacePath) _processedProjectPaths.Clear(); } + private async Task> RunForDirectoryAsnyc(string repoRootPath, string workspacePath) + { + _logger.Log($"Running for directory [{Path.GetRelativePath(repoRootPath, workspacePath)}]"); + var projectPaths = FindProjectFiles(workspacePath); + if (projectPaths.IsEmpty) + { + _logger.Log("No project files found."); + return []; + } + + return await RunForProjectPathsAsync(repoRootPath, projectPaths); + } + + private static ImmutableArray FindProjectFiles(string workspacePath) + { + return Directory.EnumerateFiles(workspacePath, "*.csproj", SearchOption.AllDirectories) + .Concat(Directory.EnumerateFiles(workspacePath, "*.vbproj", SearchOption.AllDirectories)) + .Concat(Directory.EnumerateFiles(workspacePath, "*.fsproj", SearchOption.AllDirectories)) + .ToImmutableArray(); + } + private async Task> RunForSolutionAsync(string repoRootPath, string solutionPath) { _logger.Log($"Running for solution [{Path.GetRelativePath(repoRootPath, solutionPath)}]"); @@ -91,23 +120,8 @@ private async Task> RunForSolutionAsync(s return []; } - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); var projectPaths = MSBuildHelper.GetProjectPathsFromSolution(solutionPath); - foreach (var projectPath in projectPaths) - { - var projectResults = await RunForProjectAsync(repoRootPath, projectPath); - foreach (var projectResult in projectResults) - { - if (results.ContainsKey(projectResult.FilePath)) - { - continue; - } - - results[projectResult.FilePath] = projectResult; - } - } - - return [.. results.Values]; + return await RunForProjectPathsAsync(repoRootPath, projectPaths); } private async Task> RunForProjFileAsync(string repoRootPath, string projFilePath) @@ -119,42 +133,27 @@ private async Task> RunForProjFileAsync(s return []; } - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); var projectPaths = MSBuildHelper.GetProjectPathsFromProject(projFilePath); - foreach (var projectPath in projectPaths) - { - // If there is some MSBuild logic that needs to run to fully resolve the path skip the project - if (File.Exists(projectPath)) - { - var projectResults = await RunForProjectAsync(repoRootPath, projectPath); - foreach (var projectResult in projectResults) - { - if (results.ContainsKey(projectResult.FilePath)) - { - continue; - } - - results[projectResult.FilePath] = projectResult; - } - } - } - - return [.. results.Values]; + return await RunForProjectPathsAsync(repoRootPath, projectPaths); } private async Task> RunForProjectAsync(string repoRootPath, string projectFilePath) { - var relativeProjectPath = Path.GetRelativePath(repoRootPath, projectFilePath); - _logger.Log($"Running for project file [{relativeProjectPath}]"); + _logger.Log($"Running for project file [{Path.GetRelativePath(repoRootPath, projectFilePath)}]"); if (!File.Exists(projectFilePath)) { _logger.Log($"File [{projectFilePath}] does not exist."); return []; } + var projectPaths = MSBuildHelper.GetProjectPathsFromProject(projectFilePath).Prepend(projectFilePath); + return await RunForProjectPathsAsync(repoRootPath, projectPaths); + } + + private async Task> RunForProjectPathsAsync(string repoRootPath, IEnumerable projectFilePaths) + { var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - var projectPaths = MSBuildHelper.GetProjectPathsFromProject(projectFilePath); - foreach (var projectPath in projectPaths.Prepend(projectFilePath)) + foreach (var projectPath in projectFilePaths) { // If there is some MSBuild logic that needs to run to fully resolve the path skip the project if (!File.Exists(projectPath)) @@ -162,6 +161,13 @@ private async Task> RunForProjectAsync(st continue; } + if (_processedProjectPaths.Contains(projectPath)) + { + continue; + } + _processedProjectPaths.Add(projectPath); + + var relativeProjectPath = Path.GetRelativePath(repoRootPath, projectPath); var packagesConfigDependencies = PackagesConfigDiscovery.Discover(repoRootPath, projectPath, _logger) ?.Dependencies; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs index 9b37498e2c2..743ef136f93 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs @@ -16,6 +16,7 @@ public sealed record WorkspaceDiscoveryResult : IDiscoveryResult public enum WorkspaceType { Unknown, + Directory, Solution, DirsProj, Project, From 07c04a520db68de316f8b4c3c6a1fdf4f14e1008 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 13 Mar 2024 21:21:42 -0700 Subject: [PATCH 08/26] Allow discovery output path to be specified. --- .../NuGetUpdater.Cli/Commands/DiscoverCommand.cs | 8 +++++--- .../Discover/DiscoveryWorkerTestBase.cs | 2 +- .../NuGetUpdater.Core/Discover/DiscoveryWorker.cs | 12 +++++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs index b73b93322fc..c709faba165 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs @@ -9,6 +9,7 @@ internal static class DiscoverCommand { internal static readonly Option RepoRootOption = new("--repo-root", () => new DirectoryInfo(Environment.CurrentDirectory)) { IsRequired = false }; internal static readonly Option WorkspaceOption = new("--workspace") { IsRequired = true }; + internal static readonly Option OutputOption = new("--output", () => DiscoveryWorker.DiscoveryResultFileName) { IsRequired = false }; internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); internal static Command GetCommand(Action setExitCode) @@ -17,16 +18,17 @@ internal static Command GetCommand(Action setExitCode) { RepoRootOption, WorkspaceOption, + OutputOption, VerboseOption }; command.TreatUnmatchedTokensAsErrors = true; - command.SetHandler(async (repoRoot, workspace, verbose) => + command.SetHandler(async (repoRoot, workspace, outputPath, verbose) => { var worker = new DiscoveryWorker(new Logger(verbose)); - await worker.RunAsync(repoRoot.FullName, workspace.FullName); - }, RepoRootOption, WorkspaceOption, VerboseOption); + await worker.RunAsync(repoRoot.FullName, workspace.FullName, outputPath); + }, RepoRootOption, WorkspaceOption, OutputOption, VerboseOption); return command; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index 48d62c9dcb7..22125c03b24 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -20,7 +20,7 @@ protected static async Task TestDiscovery( var actualResult = await RunDiscoveryAsync(files, async directoryPath => { var worker = new DiscoveryWorker(new Logger(verbose: true)); - await worker.RunAsync(directoryPath, workspacePath); + await worker.RunAsync(directoryPath, workspacePath, DiscoveryWorker.DiscoveryResultFileName); }); ValidateWorkspaceResult(expectedResult, actualResult); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index 89f55e516b6..427f121d18e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -6,7 +6,7 @@ namespace NuGetUpdater.Core.Discover; public partial class DiscoveryWorker { - public const string DiscoveryResultFileName = ".dependabot/discovery.json"; + public const string DiscoveryResultFileName = "./.dependabot/discovery.json"; private readonly Logger _logger; private readonly HashSet _processedProjectPaths = new(StringComparer.OrdinalIgnoreCase); @@ -22,7 +22,7 @@ public DiscoveryWorker(Logger logger) _logger = logger; } - public async Task RunAsync(string repoRootPath, string workspacePath) + public async Task RunAsync(string repoRootPath, string workspacePath, string outputPath) { MSBuildHelper.RegisterMSBuild(); @@ -83,7 +83,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath) Projects = projectResults, }; - await WriteResults(repoRootPath, result); + await WriteResults(repoRootPath, outputPath, result); _logger.Log("Discovery complete."); @@ -197,9 +197,11 @@ private async Task> RunForProjectPathsAsy return [.. results.Values]; } - private static async Task WriteResults(string repoRootPath, WorkspaceDiscoveryResult result) + private static async Task WriteResults(string repoRootPath, string outputPath, WorkspaceDiscoveryResult result) { - var resultPath = Path.GetFullPath(DiscoveryResultFileName, repoRootPath); + var resultPath = Path.IsPathRooted(outputPath) + ? outputPath + : Path.GetFullPath(outputPath, repoRootPath); var resultDirectory = Path.GetDirectoryName(resultPath)!; if (!Directory.Exists(resultDirectory)) { From 3871719eeb1066071e00bf77ac92724018e7d0b5 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 13 Mar 2024 22:40:54 -0700 Subject: [PATCH 09/26] Use the discovery results in the updater --- .../nuget/discovery/dependency_details.rb | 87 +++ .../discovery/dependency_file_discovery.rb | 102 +++ .../directory_packages_props_discovery.rb | 42 ++ .../nuget/discovery/discovery_json_reader.rb | 94 +++ .../nuget/discovery/evaluation_details.rb | 69 ++ .../nuget/discovery/project_discovery.rb | 59 ++ .../nuget/discovery/property_details.rb | 41 ++ .../nuget/discovery/workspace_discovery.rb | 80 +++ nuget/lib/dependabot/nuget/file_parser.rb | 112 +--- .../file_parser/dotnet_tools_json_parser.rb | 70 -- .../nuget/file_parser/global_json_parser.rb | 67 -- .../file_parser/packages_config_parser.rb | 91 --- .../nuget/file_parser/project_file_parser.rb | 619 ------------------ .../file_parser/property_value_finder.rb | 223 ------- nuget/lib/dependabot/nuget/file_updater.rb | 88 +-- .../file_updater/property_value_updater.rb | 80 --- nuget/lib/dependabot/nuget/native_helpers.rb | 55 ++ nuget/lib/dependabot/nuget/update_checker.rb | 5 +- .../update_checker/compatibility_checker.rb | 7 +- .../nuget/update_checker/dependency_finder.rb | 1 + .../nuget/update_checker/property_updater.rb | 1 + .../nuget/update_checker/tfm_finder.rb | 128 +--- .../nuget/update_checker/version_finder.rb | 7 +- .../property_value_updater_spec.rb | 83 --- .../compatibility_checker_spec.rb | 11 +- nuget/spec/spec_helper.rb | 1 + 26 files changed, 698 insertions(+), 1525 deletions(-) create mode 100644 nuget/lib/dependabot/nuget/discovery/dependency_details.rb create mode 100644 nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb create mode 100644 nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb create mode 100644 nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb create mode 100644 nuget/lib/dependabot/nuget/discovery/evaluation_details.rb create mode 100644 nuget/lib/dependabot/nuget/discovery/project_discovery.rb create mode 100644 nuget/lib/dependabot/nuget/discovery/property_details.rb create mode 100644 nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb delete mode 100644 nuget/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb delete mode 100644 nuget/lib/dependabot/nuget/file_parser/global_json_parser.rb delete mode 100644 nuget/lib/dependabot/nuget/file_parser/packages_config_parser.rb delete mode 100644 nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb delete mode 100644 nuget/lib/dependabot/nuget/file_parser/property_value_finder.rb delete mode 100644 nuget/lib/dependabot/nuget/file_updater/property_value_updater.rb delete mode 100644 nuget/spec/dependabot/nuget/file_updater/property_value_updater_spec.rb diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_details.rb b/nuget/lib/dependabot/nuget/discovery/dependency_details.rb new file mode 100644 index 00000000000..f2ddde19c1a --- /dev/null +++ b/nuget/lib/dependabot/nuget/discovery/dependency_details.rb @@ -0,0 +1,87 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/discovery/evaluation_details" + +module Dependabot + module Nuget + class DependencyDetails + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(DependencyDetails) } + def self.from_json(json) + name = T.let(json.fetch("Name"), String) + version = T.let(json.fetch("Version"), String) + type = T.let(json.fetch("Type"), String) + evaluation = EvaluationDetails + .from_json(T.let(json.fetch("EvaluationResult"), T.nilable(T::Hash[String, T.untyped]))) + is_dev_dependency = T.let(json.fetch("IsDevDependency"), T::Boolean) + is_direct = T.let(json.fetch("IsDirect"), T::Boolean) + is_transitive = T.let(json.fetch("IsTransitive"), T::Boolean) + is_override = T.let(json.fetch("IsOverride"), T::Boolean) + is_update = T.let(json.fetch("IsUpdate"), T::Boolean) + + DependencyDetails.new(name: name, + version: version, + type: type, + evaluation: evaluation, + is_dev_dependency: is_dev_dependency, + is_direct: is_direct, + is_transitive: is_transitive, + is_override: is_override, + is_update: is_update) + end + + sig do + params(name: String, + version: String, + type: String, + evaluation: T.nilable(EvaluationDetails), + is_dev_dependency: T::Boolean, + is_direct: T::Boolean, + is_transitive: T::Boolean, + is_override: T::Boolean, + is_update: T::Boolean).void + end + def initialize(name:, version:, type:, evaluation:, is_dev_dependency:, is_direct:, is_transitive:, + is_override:, is_update:) + @name = name + @version = version + @type = type + @evaluation = evaluation + @is_dev_dependency = is_dev_dependency + @is_direct = is_direct + @is_transitive = is_transitive + @is_override = is_override + @is_update = is_update + end + + sig { returns(String) } + attr_reader :name + + sig { returns(String) } + attr_reader :version + + sig { returns(String) } + attr_reader :type + + sig { returns(T.nilable(EvaluationDetails)) } + attr_reader :evaluation + + sig { returns(T::Boolean) } + attr_reader :is_dev_dependency + + sig { returns(T::Boolean) } + attr_reader :is_direct + + sig { returns(T::Boolean) } + attr_reader :is_transitive + + sig { returns(T::Boolean) } + attr_reader :is_override + + sig { returns(T::Boolean) } + attr_reader :is_update + end + end +end diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb new file mode 100644 index 00000000000..e92f79390f1 --- /dev/null +++ b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb @@ -0,0 +1,102 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/discovery/dependency_details" + +module Dependabot + module Nuget + class DependencyFileDiscovery + extend T::Sig + + sig { params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(DependencyFileDiscovery)) } + def self.from_json(json) + return nil if json.nil? + + file_path = T.let(json.fetch("FilePath"), String) + dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| + DependencyDetails.from_json(dep) + end + + DependencyFileDiscovery.new(file_path: file_path, + dependencies: dependencies) + end + + sig do + params(file_path: String, + dependencies: T::Array[DependencyDetails]).void + end + def initialize(file_path:, dependencies:) + @file_path = file_path + @dependencies = dependencies + end + + sig { returns(String) } + attr_reader :file_path + + sig { returns(T::Array[DependencyDetails]) } + attr_reader :dependencies + + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + def dependency_set + dependency_set = Dependabot::FileParsers::Base::DependencySet.new + + file_name = Pathname.new(file_path).cleanpath.to_path + dependencies.each do |dependency_details| + # Exclude any dependencies using version ranges or wildcards + next if dependency_details.version == "" || + dependency_details.version.include?(",") || + dependency_details.version.include?("*") + + # Exclude any dependencies specified using interpolation + next if dependency_details.name.include?("%(") || + dependency_details.version.include?("%(") + + # Exclude any dependencies that are updates + next if dependency_details.is_update + + dependency_set << build_dependency(file_name, dependency_details) + end + + dependency_set + end + + private + + sig { params(file_name: String, dependency_details: DependencyDetails).returns(Dependabot::Dependency) } + def build_dependency(file_name, dependency_details) + requirement = build_requirement(file_name, dependency_details) + requirements = requirement.nil? ? [] : [requirement] + + version = dependency_details.version.gsub(/[\(\)\[\]]/, "").strip + + Dependency.new( + name: dependency_details.name, + version: version, + package_manager: "nuget", + requirements: requirements + ) + end + + sig do + params(file_name: String, dependency_details: DependencyDetails) + .returns(T.nilable(T::Hash[Symbol, T.untyped])) + end + def build_requirement(file_name, dependency_details) + return if dependency_details.is_transitive + + requirement = { + requirement: dependency_details.version, + file: file_name, + groups: [dependency_details.is_dev_dependency ? "devDependencies" : "dependencies"], + source: nil + } + + property_name = dependency_details.evaluation&.last_property_name + return requirement unless property_name + + requirement[:metadata] = { property_name: property_name } + requirement + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb b/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb new file mode 100644 index 00000000000..52a4fa43a92 --- /dev/null +++ b/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb @@ -0,0 +1,42 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/discovery/dependency_details" + +module Dependabot + module Nuget + class DirectoryPackagesPropsDiscovery < DependencyFileDiscovery + extend T::Sig + + sig do + params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(DirectoryPackagesPropsDiscovery)) + end + def self.from_json(json) + return nil if json.nil? + + file_path = T.let(json.fetch("FilePath"), String) + is_transitive_pinning_enabled = T.let(json.fetch("IsTransitivePinningEnabled"), T::Boolean) + dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| + DependencyDetails.from_json(dep) + end + + DirectoryPackagesPropsDiscovery.new(file_path: file_path, + is_transitive_pinning_enabled: is_transitive_pinning_enabled, + dependencies: dependencies) + end + + sig do + params(file_path: String, + is_transitive_pinning_enabled: T::Boolean, + dependencies: T::Array[DependencyDetails]).void + end + def initialize(file_path:, is_transitive_pinning_enabled:, dependencies:) + super(file_path: file_path, dependencies: dependencies) + @is_transitive_pinning_enabled = is_transitive_pinning_enabled + end + + sig { returns(T::Boolean) } + attr_reader :is_transitive_pinning_enabled + end + end +end diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb new file mode 100644 index 00000000000..e6889b1c80c --- /dev/null +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -0,0 +1,94 @@ +# typed: strong +# frozen_string_literal: true + +require "json" + +require "dependabot/dependency" + +module Dependabot + module Nuget + class DiscoveryJsonReader + extend T::Sig + + DISCOVERY_JSON_PATH = ".dependabot/discovery.json" + + sig { returns(T::Boolean) } + private_class_method def self.test_run? + ENV["DEPENDABOT_NUGET_TEST_RUN"] == "true" + end + + sig { returns(String) } + private_class_method def self.temp_directory + Dir.tmpdir + end + + sig { returns(String) } + def self.discovery_file_path + File.join(temp_directory, DISCOVERY_JSON_PATH) + end + + sig { params(discovery_json: DependencyFile).void } + def self.write_discovery_json(discovery_json) + throw "Not a test run!" unless test_run? + + @test_discovery_json = T.let(discovery_json, T.nilable(DependencyFile)) + end + + sig { returns(T.nilable(DependencyFile)) } + def self.discovery_json + return @test_discovery_json if test_run? + + return unless File.exist?(discovery_file_path) + + DependencyFile.new( + name: Pathname.new(discovery_file_path).cleanpath.to_path, + directory: temp_directory, + type: "file", + content: File.read(discovery_file_path) + ) + end + + sig { params(discovery_json: DependencyFile).void } + def initialize(discovery_json:) + @discovery_json = discovery_json + end + + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + def dependency_set + dependency_set = Dependabot::FileParsers::Base::DependencySet.new + return dependency_set unless workspace_discovery + + workspace_result = T.must(workspace_discovery) + workspace_result.projects.each do |project| + dependency_set += project.dependency_set + end + if workspace_result.directory_packages_props + dependency_set += T.must(workspace_result.directory_packages_props).dependency_set + end + if workspace_result.dotnet_tools_json + dependency_set += T.must(workspace_result.dotnet_tools_json).dependency_set + end + dependency_set += T.must(workspace_result.global_json).dependency_set if workspace_result.global_json + + dependency_set + end + + sig { returns(T.nilable(WorkspaceDiscovery)) } + def workspace_discovery + @workspace_discovery ||= T.let(begin + return nil unless discovery_json.content + + parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) + WorkspaceDiscovery.from_json(parsed_json) + end, T.nilable(WorkspaceDiscovery)) + rescue JSON::ParserError + raise Dependabot::DependencyFileNotParseable, discovery_json.path + end + + private + + sig { returns(DependencyFile) } + attr_reader :discovery_json + end + end +end diff --git a/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb b/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb new file mode 100644 index 00000000000..ddac1c99f08 --- /dev/null +++ b/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb @@ -0,0 +1,69 @@ +# typed: strong +# frozen_string_literal: true + +module Dependabot + module Nuget + class EvaluationDetails + extend T::Sig + + sig { params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(EvaluationDetails)) } + def self.from_json(json) + return nil if json.nil? + + result_type = T.let(json.fetch("ResultType"), String) + original_value = T.let(json.fetch("OriginalValue"), String) + evaluated_value = T.let(json.fetch("EvaluatedValue"), String) + first_property_name = T.let(json.fetch("FirstPropertyName", nil), T.nilable(String)) + last_property_name = T.let(json.fetch("LastPropertyName", nil), T.nilable(String)) + error_message = T.let(json.fetch("ErrorMessage", nil), T.nilable(String)) + + EvaluationDetails.new(result_type: result_type, + original_value: original_value, + evaluated_value: evaluated_value, + first_property_name: first_property_name, + last_property_name: last_property_name, + error_message: error_message) + end + + sig do + params(result_type: String, + original_value: String, + evaluated_value: String, + first_property_name: T.nilable(String), + last_property_name: T.nilable(String), + error_message: T.nilable(String)).void + end + def initialize(result_type:, + original_value:, + evaluated_value:, + first_property_name:, + last_property_name:, + error_message:) + @result_type = result_type + @original_value = original_value + @evaluated_value = evaluated_value + @first_property_name = first_property_name + @last_property_name = last_property_name + @error_message = error_message + end + + sig { returns(String) } + attr_reader :result_type + + sig { returns(String) } + attr_reader :original_value + + sig { returns(String) } + attr_reader :evaluated_value + + sig { returns(T.nilable(String)) } + attr_reader :first_property_name + + sig { returns(T.nilable(String)) } + attr_reader :last_property_name + + sig { returns(T.nilable(String)) } + attr_reader :error_message + end + end +end diff --git a/nuget/lib/dependabot/nuget/discovery/project_discovery.rb b/nuget/lib/dependabot/nuget/discovery/project_discovery.rb new file mode 100644 index 00000000000..66cc9e46579 --- /dev/null +++ b/nuget/lib/dependabot/nuget/discovery/project_discovery.rb @@ -0,0 +1,59 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/discovery/dependency_details" +require "dependabot/nuget/discovery/property_details" + +module Dependabot + module Nuget + class ProjectDiscovery < DependencyFileDiscovery + extend T::Sig + + sig do + params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(ProjectDiscovery)) + end + def self.from_json(json) + return nil if json.nil? + + file_path = T.let(json.fetch("FilePath"), String) + properties = T.let(json.fetch("Properties"), T::Hash[String, T::Hash[String, T.untyped]]).values.map do |prop| + PropertyDetails.from_json(prop) + end + target_frameworks = T.let(json.fetch("TargetFrameworks"), T::Array[String]) + referenced_project_paths = T.let(json.fetch("ReferencedProjectPaths"), T::Array[String]) + dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| + DependencyDetails.from_json(dep) + end + + ProjectDiscovery.new(file_path: file_path, + properties: properties, + target_frameworks: target_frameworks, + referenced_project_paths: referenced_project_paths, + dependencies: dependencies) + end + + sig do + params(file_path: String, + properties: T::Array[PropertyDetails], + target_frameworks: T::Array[String], + referenced_project_paths: T::Array[String], + dependencies: T::Array[DependencyDetails]).void + end + def initialize(file_path:, properties:, target_frameworks:, referenced_project_paths:, dependencies:) + super(file_path: file_path, dependencies: dependencies) + @properties = properties + @target_frameworks = target_frameworks + @referenced_project_paths = referenced_project_paths + end + + sig { returns(T::Array[PropertyDetails]) } + attr_reader :properties + + sig { returns(T::Array[String]) } + attr_reader :target_frameworks + + sig { returns(T::Array[String]) } + attr_reader :referenced_project_paths + end + end +end diff --git a/nuget/lib/dependabot/nuget/discovery/property_details.rb b/nuget/lib/dependabot/nuget/discovery/property_details.rb new file mode 100644 index 00000000000..9daa7e8bb24 --- /dev/null +++ b/nuget/lib/dependabot/nuget/discovery/property_details.rb @@ -0,0 +1,41 @@ +# typed: strong +# frozen_string_literal: true + +module Dependabot + module Nuget + class PropertyDetails + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(PropertyDetails) } + def self.from_json(json) + name = T.let(json.fetch("Name"), String) + value = T.let(json.fetch("Value"), String) + source_file_path = T.let(json.fetch("SourceFilePath"), String) + + PropertyDetails.new(name: name, + value: value, + source_file_path: source_file_path) + end + + sig do + params(name: String, + value: String, + source_file_path: String).void + end + def initialize(name:, value:, source_file_path:) + @name = name + @value = value + @source_file_path = source_file_path + end + + sig { returns(String) } + attr_reader :name + + sig { returns(String) } + attr_reader :value + + sig { returns(String) } + attr_reader :source_file_path + end + end +end diff --git a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb new file mode 100644 index 00000000000..4c5b6ed2189 --- /dev/null +++ b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb @@ -0,0 +1,80 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/discovery/dependency_file_discovery" +require "dependabot/nuget/discovery/directory_packages_props_discovery" +require "dependabot/nuget/discovery/project_discovery" + +module Dependabot + module Nuget + class WorkspaceDiscovery + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(WorkspaceDiscovery) } + def self.from_json(json) + file_path = T.let(json.fetch("FilePath"), String) + type = T.let(json.fetch("Type"), String) + target_frameworks = T.let(json.fetch("TargetFrameworks"), T::Array[String]) + projects = T.let(json.fetch("Projects"), T::Array[T::Hash[String, T.untyped]]).filter_map do |project| + ProjectDiscovery.from_json(project) + end + directory_packages_props = DirectoryPackagesPropsDiscovery + .from_json(T.let(json.fetch("DirectoryPackagesProps"), + T.nilable(T::Hash[String, T.untyped]))) + global_json = DependencyFileDiscovery + .from_json(T.let(json.fetch("GlobalJson"), T.nilable(T::Hash[String, T.untyped]))) + dotnet_tools_json = DependencyFileDiscovery + .from_json(T.let(json.fetch("DotNetToolsJson"), T.nilable(T::Hash[String, T.untyped]))) + + WorkspaceDiscovery.new(file_path: file_path, + type: type, + target_frameworks: target_frameworks, + projects: projects, + directory_packages_props: directory_packages_props, + global_json: global_json, + dotnet_tools_json: dotnet_tools_json) + end + + sig do + params(file_path: String, + type: String, + target_frameworks: T::Array[String], + projects: T::Array[ProjectDiscovery], + directory_packages_props: T.nilable(DirectoryPackagesPropsDiscovery), + global_json: T.nilable(DependencyFileDiscovery), + dotnet_tools_json: T.nilable(DependencyFileDiscovery)).void + end + def initialize(file_path:, type:, target_frameworks:, projects:, directory_packages_props:, global_json:, + dotnet_tools_json:) + @file_path = file_path + @type = type + @target_frameworks = target_frameworks + @projects = projects + @directory_packages_props = directory_packages_props + @global_json = global_json + @dotnet_tools_json = dotnet_tools_json + end + + sig { returns(String) } + attr_reader :file_path + + sig { returns(String) } + attr_reader :type + + sig { returns(T::Array[String]) } + attr_reader :target_frameworks + + sig { returns(T::Array[ProjectDiscovery]) } + attr_reader :projects + + sig { returns(T.nilable(DirectoryPackagesPropsDiscovery)) } + attr_reader :directory_packages_props + + sig { returns(T.nilable(DependencyFileDiscovery)) } + attr_reader :global_json + + sig { returns(T.nilable(DependencyFileDiscovery)) } + attr_reader :dotnet_tools_json + end + end +end diff --git a/nuget/lib/dependabot/nuget/file_parser.rb b/nuget/lib/dependabot/nuget/file_parser.rb index cd12fa6ab0e..0390740ec89 100644 --- a/nuget/lib/dependabot/nuget/file_parser.rb +++ b/nuget/lib/dependabot/nuget/file_parser.rb @@ -1,11 +1,10 @@ # typed: strong # frozen_string_literal: true -require "nokogiri" - require "dependabot/dependency" require "dependabot/file_parsers" require "dependabot/file_parsers/base" +require "dependabot/nuget/discovery/discovery_json_reader" require "sorbet-runtime" # For details on how dotnet handles version constraints, see: @@ -16,87 +15,31 @@ class FileParser < Dependabot::FileParsers::Base extend T::Sig require "dependabot/file_parsers/base/dependency_set" - require_relative "file_parser/project_file_parser" - require_relative "file_parser/packages_config_parser" - require_relative "file_parser/global_json_parser" - require_relative "file_parser/dotnet_tools_json_parser" - - PACKAGE_CONF_DEPENDENCY_SELECTOR = "packages > packages" sig { override.returns(T::Array[Dependabot::Dependency]) } def parse - dependency_set = DependencySet.new - dependency_set += project_file_dependencies - dependency_set += packages_config_dependencies - dependency_set += global_json_dependencies if global_json - dependency_set += dotnet_tools_json_dependencies if dotnet_tools_json + workspace_path = project_files.first&.directory + return [] unless workspace_path - (dependencies, deps_with_unresolved_versions) = dependency_set.dependencies.partition do |d| - # try to parse the version; don't care about result, just that it succeeded - _ = Version.new(d.version) - true - rescue ArgumentError - # version could not be parsed - false - end - - deps_with_unresolved_versions.each do |d| - Dependabot.logger.warn "Dependency '#{d.name}' excluded due to unparsable version: #{d.version}" - end + # run discovery for the repo + NativeHelpers.run_nuget_discover_tool(repo_root: T.must(repo_contents_path), + workspace_path: workspace_path, + output_path: DiscoveryJsonReader.discovery_file_path, + credentials: credentials) - dependencies + discovered_dependencies.dependencies end private sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def project_file_dependencies - dependency_set = DependencySet.new - - (project_files + project_import_files).each do |file| - parser = project_file_parser - dependency_set += parser.dependency_set(project_file: file) - end - - dependency_set - end - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def packages_config_dependencies - dependency_set = DependencySet.new - - packages_config_files.each do |file| - parser = PackagesConfigParser.new(packages_config: file) - dependency_set += parser.dependency_set - end - - dependency_set - end - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def global_json_dependencies - return DependencySet.new unless global_json + def discovered_dependencies + discovery_json = DiscoveryJsonReader.discovery_json + return DependencySet.new unless discovery_json - GlobalJsonParser.new(global_json: T.must(global_json)).dependency_set - end - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def dotnet_tools_json_dependencies - return DependencySet.new unless dotnet_tools_json - - DotNetToolsJsonParser.new(dotnet_tools_json: T.must(dotnet_tools_json)).dependency_set - end - - sig { returns(Dependabot::Nuget::FileParser::ProjectFileParser) } - def project_file_parser - @project_file_parser ||= T.let( - ProjectFileParser.new( - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: @repo_contents_path - ), - T.nilable(Dependabot::Nuget::FileParser::ProjectFileParser) - ) + DiscoveryJsonReader.new( + discovery_json: discovery_json + ).dependency_set end sig { returns(T::Array[Dependabot::DependencyFile]) } @@ -117,31 +60,6 @@ def packages_config_files end end - sig { returns(T::Array[Dependabot::DependencyFile]) } - def project_import_files - dependency_files - - project_files - - packages_config_files - - nuget_configs - - [global_json] - - [dotnet_tools_json] - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def nuget_configs - dependency_files.select { |f| f.name.match?(/nuget\.config$/i) } - end - - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def global_json - dependency_files.find { |f| f.name.casecmp("global.json")&.zero? } - end - - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def dotnet_tools_json - dependency_files.find { |f| f.name.casecmp(".config/dotnet-tools.json")&.zero? } - end - sig { override.void } def check_required_files return if project_files.any? || packages_config_files.any? diff --git a/nuget/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb b/nuget/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb deleted file mode 100644 index 71df70a5977..00000000000 --- a/nuget/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb +++ /dev/null @@ -1,70 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "json" - -require "dependabot/dependency" -require "dependabot/nuget/file_parser" - -# For details on dotnet-tools.json files see: -# https://learn.microsoft.com/en-us/dotnet/core/tools/local-tools-how-to-use -module Dependabot - module Nuget - class FileParser - class DotNetToolsJsonParser - extend T::Sig - - require "dependabot/file_parsers/base/dependency_set" - - sig { params(dotnet_tools_json: Dependabot::DependencyFile).void } - def initialize(dotnet_tools_json:) - @dotnet_tools_json = dotnet_tools_json - @parsed_dotnet_tools_json = T.let(nil, T.nilable(T::Hash[String, T.untyped])) - end - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def dependency_set - dependency_set = Dependabot::FileParsers::Base::DependencySet.new - - tools = parsed_dotnet_tools_json.fetch("tools", {}) - - raise Dependabot::DependencyFileNotParseable, dotnet_tools_json.path unless tools.is_a?(Hash) - - tools.each do |dependency_name, node| - raise Dependabot::DependencyFileNotParseable, dotnet_tools_json.path unless node.is_a?(Hash) - - version = node["version"] - dependency_set << - Dependency.new( - name: dependency_name, - version: version, - package_manager: "nuget", - requirements: [{ - requirement: version, - file: dotnet_tools_json.name, - groups: ["dependencies"], - source: nil - }] - ) - end - - dependency_set - end - - private - - sig { returns(Dependabot::DependencyFile) } - attr_reader :dotnet_tools_json - - sig { returns(T::Hash[String, T.untyped]) } - def parsed_dotnet_tools_json - # Remove BOM if present as JSON should be UTF-8 - content = T.must(dotnet_tools_json.content) - @parsed_dotnet_tools_json ||= JSON.parse(content.delete_prefix("\uFEFF")) - rescue JSON::ParserError - raise Dependabot::DependencyFileNotParseable, dotnet_tools_json.path - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/file_parser/global_json_parser.rb b/nuget/lib/dependabot/nuget/file_parser/global_json_parser.rb deleted file mode 100644 index 33223b67707..00000000000 --- a/nuget/lib/dependabot/nuget/file_parser/global_json_parser.rb +++ /dev/null @@ -1,67 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "json" - -require "dependabot/dependency" -require "dependabot/nuget/file_parser" - -# For details on global.json files see: -# https://docs.microsoft.com/en-us/dotnet/core/tools/global-json -module Dependabot - module Nuget - class FileParser - class GlobalJsonParser - extend T::Sig - - require "dependabot/file_parsers/base/dependency_set" - - sig { params(global_json: Dependabot::DependencyFile).void } - def initialize(global_json:) - @global_json = global_json - @parsed_global_json = T.let(nil, T.nilable(T::Hash[String, T.untyped])) - end - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def dependency_set - dependency_set = Dependabot::FileParsers::Base::DependencySet.new - - project_sdks = parsed_global_json.fetch("msbuild-sdks", {}) - - raise Dependabot::DependencyFileNotParseable, global_json.path unless project_sdks.is_a?(Hash) - - project_sdks.each do |dependency_name, version| - dependency_set << - Dependency.new( - name: dependency_name, - version: version, - package_manager: "nuget", - requirements: [{ - requirement: version, - file: global_json.name, - groups: ["dependencies"], - source: nil - }] - ) - end - - dependency_set - end - - private - - sig { returns(Dependabot::DependencyFile) } - attr_reader :global_json - - sig { returns(T::Hash[String, T.untyped]) } - def parsed_global_json - # Remove BOM if present as JSON should be UTF-8 - content = T.must(global_json.content) - @parsed_global_json ||= JSON.parse(content.delete_prefix("\uFEFF")) - rescue JSON::ParserError - raise Dependabot::DependencyFileNotParseable, global_json.path - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/file_parser/packages_config_parser.rb b/nuget/lib/dependabot/nuget/file_parser/packages_config_parser.rb deleted file mode 100644 index 815058707fa..00000000000 --- a/nuget/lib/dependabot/nuget/file_parser/packages_config_parser.rb +++ /dev/null @@ -1,91 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" - -require "dependabot/dependency" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/cache_manager" - -# For details on packages.config files see: -# https://docs.microsoft.com/en-us/nuget/reference/packages-config -module Dependabot - module Nuget - class FileParser - class PackagesConfigParser - extend T::Sig - require "dependabot/file_parsers/base/dependency_set" - - DEPENDENCY_SELECTOR = "packages > package" - - sig { returns(T::Hash[String, Dependabot::FileParsers::Base::DependencySet]) } - def self.dependency_set_cache - CacheManager.cache("packages_config_dependency_set") - end - - sig { params(packages_config: Dependabot::DependencyFile).void } - def initialize(packages_config:) - @packages_config = packages_config - end - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def dependency_set - key = "#{packages_config.name.downcase}::#{packages_config.content.hash}" - cache = PackagesConfigParser.dependency_set_cache - - cache[key] ||= parse_dependencies - end - - private - - sig { returns(Dependabot::DependencyFile) } - attr_reader :packages_config - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def parse_dependencies - dependency_set = Dependabot::FileParsers::Base::DependencySet.new - - doc = Nokogiri::XML(packages_config.content) - doc.remove_namespaces! - doc.css(DEPENDENCY_SELECTOR).each do |dependency_node| - dependency_set << - Dependency.new( - name: T.must(dependency_name(dependency_node)), - version: dependency_version(dependency_node), - package_manager: "nuget", - requirements: [{ - requirement: dependency_version(dependency_node), - file: packages_config.name, - groups: [dependency_type(dependency_node)], - source: nil - }] - ) - end - - dependency_set - end - - sig { params(dependency_node: Nokogiri::XML::Node).returns(T.nilable(String)) } - def dependency_name(dependency_node) - dependency_node.attribute("id")&.value&.strip || - dependency_node.at_xpath("./id")&.content&.strip - end - - sig { params(dependency_node: Nokogiri::XML::Node).returns(T.nilable(String)) } - def dependency_version(dependency_node) - # Ranges and wildcards aren't allowed in a packages.config - the - # specified requirement is always an exact version. - dependency_node.attribute("version")&.value&.strip || - dependency_node.at_xpath("./version")&.content&.strip - end - - sig { params(dependency_node: Nokogiri::XML::Node).returns(String) } - def dependency_type(dependency_node) - val = dependency_node.attribute("developmentDependency")&.value&.strip || - dependency_node.at_xpath("./developmentDependency")&.content&.strip - val.to_s.casecmp("true").zero? ? "devDependencies" : "dependencies" - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb b/nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb deleted file mode 100644 index d725e82ca8e..00000000000 --- a/nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb +++ /dev/null @@ -1,619 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" - -require "dependabot/dependency" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker" -require "dependabot/nuget/cache_manager" -require "dependabot/nuget/nuget_client" - -# For details on how dotnet handles version constraints, see: -# https://docs.microsoft.com/en-us/nuget/reference/package-versioning -module Dependabot - module Nuget - class FileParser - class ProjectFileParser # rubocop:disable Metrics/ClassLength - extend T::Sig - - require "dependabot/file_parsers/base/dependency_set" - require_relative "property_value_finder" - require_relative "../update_checker/repository_finder" - - DEPENDENCY_SELECTOR = "ItemGroup > PackageReference, " \ - "ItemGroup > GlobalPackageReference, " \ - "ItemGroup > PackageVersion, " \ - "ItemGroup > Dependency, " \ - "ItemGroup > DevelopmentDependency" - - PROJECT_REFERENCE_SELECTOR = "ItemGroup > ProjectReference" - - PROJECT_FILE_SELECTOR = "ItemGroup > ProjectFile" - - PACKAGE_REFERENCE_SELECTOR = "ItemGroup > PackageReference, " \ - "ItemGroup > GlobalPackageReference" - - PACKAGE_VERSION_SELECTOR = "ItemGroup > PackageVersion" - - PROJECT_SDK_REGEX = %r{^([^/]+)/(\d+(?:[.]\d+(?:[.]\d+)?)?(?:[+-].*)?)$} - PROPERTY_REGEX = /\$\((?.*?)\)/ - ITEM_REGEX = /\@\((?.*?)\)/ - - sig { returns(T::Hash[String, Dependabot::FileParsers::Base::DependencySet]) } - def self.dependency_set_cache - CacheManager.cache("project_file_dependency_set") - end - - sig { returns(T::Hash[String, T.untyped]) } - def self.dependency_url_search_cache - CacheManager.cache("dependency_url_search_cache") - end - - sig do - params(dependency_files: T::Array[DependencyFile], - credentials: T::Array[Credential], - repo_contents_path: T.nilable(String)).void - end - def initialize(dependency_files:, credentials:, repo_contents_path:) - @dependency_files = dependency_files - @credentials = credentials - @repo_contents_path = repo_contents_path - end - - sig do - params(project_file: DependencyFile, visited_project_files: T::Set[String]) - .returns(Dependabot::FileParsers::Base::DependencySet) - end - def dependency_set(project_file:, visited_project_files: Set.new) - key = "#{project_file.name.downcase}::#{project_file.content.hash}" - cache = ProjectFileParser.dependency_set_cache - - visited_project_files.add(cache[key]) - - # Pass the visited_project_files set to parse_dependencies - cache[key] ||= parse_dependencies(project_file, visited_project_files) - end - - sig { params(project_file: DependencyFile).returns(T::Set[String]) } - def downstream_file_references(project_file:) - file_set = T.let(Set.new, T::Set[String]) - - doc = Nokogiri::XML(project_file.content) - doc.remove_namespaces! - proj_refs = doc.css(PROJECT_REFERENCE_SELECTOR) - proj_files = doc.css(PROJECT_FILE_SELECTOR) - ref_nodes = proj_refs + proj_files - ref_nodes.each do |project_reference_node| - dep_file = get_attribute_value(project_reference_node, "Include") - next unless dep_file - - full_project_path = full_path(project_file, dep_file) - full_project_path = full_project_path[1..-1] if full_project_path.start_with?("/") - full_project_paths = expand_wildcards_in_project_reference_path(T.must(full_project_path)) - full_project_paths.each do |full_project_path_expanded| - file_set << full_project_path_expanded if full_project_path_expanded - end - end - - file_set - end - - sig { params(project_file: DependencyFile).returns(T::Array[String]) } - def target_frameworks(project_file:) - target_framework = details_for_property("TargetFramework", project_file) - return [target_framework.fetch(:value)] if target_framework - - target_frameworks = details_for_property("TargetFrameworks", project_file) - return target_frameworks.fetch(:value)&.split(";") if target_frameworks - - target_framework = details_for_property("TargetFrameworkVersion", project_file) - return [] unless target_framework - - # TargetFrameworkVersion is a string like "v4.7.2" - value = target_framework.fetch(:value) - # convert it to a string like "net472" - ["net#{value[1..-1].delete('.')}"] - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def nuget_configs - dependency_files.select { |f| f.name.match?(%r{(^|/)nuget\.config$}i) } - end - - private - - sig { returns(T::Array[DependencyFile]) } - attr_reader :dependency_files - - sig { returns(T::Array[Credential]) } - attr_reader :credentials - - sig { params(project_file: DependencyFile, ref_path: String).returns(String) } - def full_path(project_file, ref_path) - project_file_directory = File.dirname(project_file.name) - is_rooted = project_file_directory.start_with?("/") - # Root the directory path to avoid expand_path prepending the working directory - project_file_directory = "/" + project_file_directory unless is_rooted - - # normalize path separators - relative_path = ref_path.tr("\\", "/") - # path is relative to the project file directory - relative_path = File.join(project_file_directory, relative_path) - result = File.expand_path(relative_path) - result = result[1..-1] unless is_rooted - T.must(result) - end - - sig do - params(project_file: DependencyFile, visited_project_files: T.untyped) - .returns(Dependabot::FileParsers::Base::DependencySet) - end - def parse_dependencies(project_file, visited_project_files) - dependency_set = Dependabot::FileParsers::Base::DependencySet.new - - doc = Nokogiri::XML(project_file.content) - doc.remove_namespaces! - # Look for regular package references - doc.css(DEPENDENCY_SELECTOR).each do |dependency_node| - name = dependency_name(dependency_node, project_file) - req = dependency_requirement(dependency_node, project_file) - version = dependency_version(dependency_node, project_file) - prop_name = req_property_name(dependency_node) - is_dev = dependency_node.name == "DevelopmentDependency" - - dependency = build_dependency(name, req, version, prop_name, project_file, dev: is_dev) - dependency_set << dependency if dependency - end - - add_global_package_references(dependency_set) - - add_transitive_dependencies(project_file, doc, dependency_set, visited_project_files) - - # Look for SDK references; see: - # https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk - add_sdk_references(doc, dependency_set, project_file) - - dependency_set - end - - sig { params(dependency_set: Dependabot::FileParsers::Base::DependencySet).void } - def add_global_package_references(dependency_set) - project_import_files.each do |file| - doc = Nokogiri::XML(file.content) - doc.remove_namespaces! - - doc.css(PACKAGE_REFERENCE_SELECTOR).each do |dependency_node| - name = dependency_name(dependency_node, file) - req = dependency_requirement(dependency_node, file) - version = dependency_version(dependency_node, file) - prop_name = req_property_name(dependency_node) - - dependency = build_dependency(name, req, version, prop_name, file) - dependency_set << dependency if dependency - end - end - end - - sig do - params(project_file: DependencyFile, - doc: Nokogiri::XML::Document, - dependency_set: Dependabot::FileParsers::Base::DependencySet, - visited_project_files: T::Set[String]) - .void - end - def add_transitive_dependencies(project_file, doc, dependency_set, visited_project_files) - add_transitive_dependencies_from_packages(dependency_set) - add_transitive_dependencies_from_project_references(project_file, doc, dependency_set, visited_project_files) - end - - sig do - params(project_file: DependencyFile, - doc: Nokogiri::XML::Document, - dependency_set: Dependabot::FileParsers::Base::DependencySet, - visited_project_files: T::Set[String]) - .void - end - def add_transitive_dependencies_from_project_references(project_file, doc, dependency_set, - visited_project_files) - - # if visited_project_files is an empty set then new up a new set - visited_project_files = Set.new if visited_project_files.nil? - # Look for regular project references - project_refs = doc.css(PROJECT_REFERENCE_SELECTOR) - # Look for ProjectFile references (dirs.proj) - project_files = doc.css(PROJECT_FILE_SELECTOR) - ref_nodes = project_refs + project_files - - ref_nodes.each do |reference_node| - relative_path = dependency_name(reference_node, project_file) - # This could result from a item. - next unless relative_path - - full_project_path = full_path(project_file, relative_path) - - full_project_paths = expand_wildcards_in_project_reference_path(full_project_path) - - full_project_paths.each do |path| - # Check if we've already visited this project file - next if visited_project_files.include?(path) - - visited_project_files.add(path) - referenced_file = dependency_files.find { |f| f.name == path } - next unless referenced_file - - dependency_set(project_file: referenced_file, - visited_project_files: visited_project_files).dependencies.each do |dep| - dependency = Dependency.new( - name: dep.name, - version: dep.version, - package_manager: dep.package_manager, - requirements: [] - ) - dependency_set << dependency - end - end - end - end - - sig { params(full_path: String).returns(T::Array[T.nilable(String)]) } - def expand_wildcards_in_project_reference_path(full_path) - full_path = File.join(@repo_contents_path, full_path) - - # For each expanded path, remove the @repo_contents_path prefix and leading slash - filtered_paths = Dir.glob(full_path).map do |path| - # Remove @repo_contents_path prefix - path = path.sub(@repo_contents_path, "") if @repo_contents_path - # Remove leading slash - path = path[1..-1] if path.start_with?("/") - path # Return the modified path - end - - return filtered_paths if filtered_paths.any? - - # If the wildcard didn't match anything, strip the @repo_contents_path prefix and return the original path. - full_path = full_path.sub(@repo_contents_path, "") if @repo_contents_path - full_path = full_path[1..-1] if full_path.start_with?("/") - [full_path] - end - - sig { params(dependency_set: Dependabot::FileParsers::Base::DependencySet).void } - def add_transitive_dependencies_from_packages(dependency_set) - transitive_dependencies_from_packages(dependency_set.dependencies).each { |dep| dependency_set << dep } - end - - sig { params(dependencies: T::Array[Dependency]).returns(T::Array[Dependency]) } - def transitive_dependencies_from_packages(dependencies) - transitive_dependencies = {} - - dependencies.each do |dependency| - UpdateChecker::DependencyFinder.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: @repo_contents_path - ).transitive_dependencies.each do |transitive_dep| - visited_dep = transitive_dependencies[transitive_dep.name.downcase] - next if !visited_dep.nil? && visited_dep.numeric_version > transitive_dep.numeric_version - - transitive_dependencies[transitive_dep.name.downcase] = transitive_dep - end - end - - transitive_dependencies.values - end - - sig do - params(doc: Nokogiri::XML::Document, - dependency_set: Dependabot::FileParsers::Base::DependencySet, - project_file: DependencyFile).void - end - def add_sdk_references(doc, dependency_set, project_file) - # These come in 3 flavours: - # - - # - - # - - # None of these support the use of properties, nor do they allow child - # elements instead of attributes. - add_sdk_refs_from_project(doc, dependency_set, project_file) - add_sdk_refs_from_sdk_tags(doc, dependency_set, project_file) - add_sdk_refs_from_import_tags(doc, dependency_set, project_file) - end - - sig do - params(sdk_references: String, - dependency_set: Dependabot::FileParsers::Base::DependencySet, - project_file: DependencyFile).void - end - def add_sdk_ref_from_project(sdk_references, dependency_set, project_file) - sdk_references.split(";").each do |sdk_reference| - m = sdk_reference.match(PROJECT_SDK_REGEX) - if m - dependency = build_dependency(m[1], m[2], m[2], nil, project_file) - dependency_set << dependency if dependency - end - end - end - - sig do - params(doc: Nokogiri::XML::Document, - dependency_set: Dependabot::FileParsers::Base::DependencySet, - project_file: DependencyFile).void - end - def add_sdk_refs_from_import_tags(doc, dependency_set, project_file) - doc.xpath("/Project/Import").each do |import_node| - next unless import_node.attribute("Sdk") && import_node.attribute("Version") - - name = import_node.attribute("Sdk")&.value&.strip - version = import_node.attribute("Version")&.value&.strip - - dependency = build_dependency(name, version, version, nil, project_file) - dependency_set << dependency if dependency - end - end - - sig do - params(doc: Nokogiri::XML::Document, - dependency_set: Dependabot::FileParsers::Base::DependencySet, - project_file: DependencyFile).void - end - def add_sdk_refs_from_project(doc, dependency_set, project_file) - doc.xpath("/Project").each do |project_node| - sdk_references = project_node.attribute("Sdk")&.value&.strip - next unless sdk_references - - add_sdk_ref_from_project(sdk_references, dependency_set, project_file) - end - end - - sig do - params(doc: Nokogiri::XML::Document, - dependency_set: Dependabot::FileParsers::Base::DependencySet, - project_file: DependencyFile).void - end - def add_sdk_refs_from_sdk_tags(doc, dependency_set, project_file) - doc.xpath("/Project/Sdk").each do |sdk_node| - next unless sdk_node.attribute("Version") - - name = sdk_node.attribute("Name")&.value&.strip - version = sdk_node.attribute("Version")&.value&.strip - - dependency = build_dependency(name, version, version, nil, project_file) - dependency_set << dependency if dependency - end - end - - sig do - params(name: T.nilable(String), - req: T.nilable(String), - version: T.nilable(String), - prop_name: T.nilable(String), - project_file: Dependabot::DependencyFile, - dev: T.untyped) - .returns(T.nilable(Dependabot::Dependency)) - end - def build_dependency(name, req, version, prop_name, project_file, dev: false) - return unless name - - # Exclude any dependencies specified using interpolation - return if [name, req, version].any? { |s| s&.include?("%(") } - - requirement = { - requirement: req, - file: project_file.name, - groups: [dev ? "devDependencies" : "dependencies"], - source: nil - } - - if prop_name - # Get the root property name unless no details could be found, - # in which case use the top-level name to ease debugging - root_prop_name = details_for_property(prop_name, project_file) - &.fetch(:root_property_name) || prop_name - requirement[:metadata] = { property_name: root_prop_name } - end - - dependency = Dependency.new( - name: name, - version: version, - package_manager: "nuget", - requirements: [requirement] - ) - - # only include dependency if one of the sources has it - return unless dependency_has_search_results?(dependency) - - dependency - end - - sig { params(dependency: Dependency).returns(T::Boolean) } - def dependency_has_search_results?(dependency) - dependency_urls = RepositoryFinder.new( - dependency: dependency, - credentials: credentials, - config_files: nuget_configs - ).dependency_urls - dependency_urls = [RepositoryFinder.get_default_repository_details(dependency.name)] if dependency_urls.empty? - dependency_urls.any? do |dependency_url| - dependency_url_has_matching_result?(dependency.name, dependency_url) - end - end - - sig { params(dependency_name: String, dependency_url: T::Hash[Symbol, String]).returns(T.nilable(T::Boolean)) } - def dependency_url_has_matching_result?(dependency_name, dependency_url) - versions = NugetClient.get_package_versions(dependency_name, dependency_url) - versions&.any? - end - - sig { params(dependency_node: Nokogiri::XML::Node, project_file: DependencyFile).returns(T.nilable(String)) } - def dependency_name(dependency_node, project_file) - raw_name = get_attribute_value(dependency_node, "Include") || - get_attribute_value(dependency_node, "Update") - return unless raw_name - - # If the item contains @(ItemGroup) then ignore as it - # updates a set of ItemGroup elements - return if raw_name.match?(ITEM_REGEX) - - evaluated_value(raw_name, project_file) - end - - sig { params(dependency_node: Nokogiri::XML::Node, project_file: DependencyFile).returns(T.nilable(String)) } - def dependency_requirement(dependency_node, project_file) - raw_requirement = get_node_version_value(dependency_node) || - find_package_version(dependency_node, project_file) - return unless raw_requirement - - evaluated_value(raw_requirement, project_file) - end - - sig { params(dependency_node: Nokogiri::XML::Node, project_file: DependencyFile).returns(T.nilable(String)) } - def find_package_version(dependency_node, project_file) - name = dependency_name(dependency_node, project_file) - return unless name - - package_version_string = package_versions[name].to_s - return unless package_version_string != "" - - package_version_string - end - - sig { returns(T::Hash[String, String]) } - def package_versions - @package_versions ||= T.let(parse_package_versions, T.nilable(T::Hash[String, String])) - end - - sig { returns(T::Hash[String, String]) } - def parse_package_versions - package_versions = T.let({}, T::Hash[String, String]) - directory_packages_props_files.each do |file| - doc = Nokogiri::XML(file.content) - doc.remove_namespaces! - doc.css(PACKAGE_VERSION_SELECTOR).each do |package_node| - name = dependency_name(package_node, file) - version = dependency_version(package_node, file) - next unless name && version - - package_versions[name] = version - end - end - package_versions - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def directory_packages_props_files - dependency_files.select { |df| df.name.match?(/[Dd]irectory.[Pp]ackages.props/) } - end - - sig { params(dependency_node: Nokogiri::XML::Node, project_file: DependencyFile).returns(T.nilable(String)) } - def dependency_version(dependency_node, project_file) - requirement = dependency_requirement(dependency_node, project_file) - return unless requirement - - # Remove brackets if present - version = requirement.gsub(/[\(\)\[\]]/, "").strip - - # We don't know the version for range requirements or wildcard - # requirements, so return `nil` for these. - return if version.include?(",") || version.include?("*") || - version == "" - - version - end - - sig { params(dependency_node: Nokogiri::XML::Node).returns(T.nilable(String)) } - def req_property_name(dependency_node) - raw_requirement = get_node_version_value(dependency_node) - return unless raw_requirement - - return unless raw_requirement.match?(PROPERTY_REGEX) - - T.must(raw_requirement.match(PROPERTY_REGEX)) - .named_captures.fetch("property") - end - - sig { params(node: Nokogiri::XML::Node).returns(T.nilable(String)) } - def get_node_version_value(node) - get_attribute_value(node, "Version") || get_attribute_value(node, "VersionOverride") - end - - # rubocop:disable Metrics/PerceivedComplexity - sig { params(node: Nokogiri::XML::Node, attribute: String).returns(T.nilable(String)) } - def get_attribute_value(node, attribute) - value = - node.attribute(attribute)&.value&.strip || - node.at_xpath("./#{attribute}")&.content&.strip || - node.attribute(attribute.downcase)&.value&.strip || - node.at_xpath("./#{attribute.downcase}")&.content&.strip - - value == "" ? nil : value - end - # rubocop:enable Metrics/PerceivedComplexity - - sig { params(value: String, project_file: Dependabot::DependencyFile).returns(String) } - def evaluated_value(value, project_file) - return value unless value.match?(PROPERTY_REGEX) - - property_name = T.must(value.match(PROPERTY_REGEX)&.named_captures&.fetch("property")) - property_details = details_for_property(property_name, project_file) - - # Don't halt parsing for a missing property value until we're - # confident we're fetching property values correctly - return value unless property_details&.fetch(:value) - - value.gsub(PROPERTY_REGEX, property_details.fetch(:value)) - end - - sig do - params(property_name: String, project_file: Dependabot::DependencyFile) - .returns(T.nilable(T::Hash[T.untyped, T.untyped])) - end - def details_for_property(property_name, project_file) - property_value_finder - .property_details( - property_name: property_name, - callsite_file: project_file - ) - end - - sig { returns(PropertyValueFinder) } - def property_value_finder - @property_value_finder ||= - T.let(PropertyValueFinder.new(dependency_files: dependency_files), T.nilable(PropertyValueFinder)) - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def project_import_files - dependency_files - - project_files - - packages_config_files - - nuget_configs - - [global_json] - - [dotnet_tools_json] - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def project_files - dependency_files.select { |f| f.name.match?(/\.[a-z]{2}proj$/) } - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def packages_config_files - dependency_files.select do |f| - f.name.split("/").last&.casecmp("packages.config")&.zero? - end - end - - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def global_json - dependency_files.find { |f| f.name.casecmp("global.json")&.zero? } - end - - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def dotnet_tools_json - dependency_files.find { |f| f.name.casecmp(".config/dotnet-tools.json")&.zero? } - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/file_parser/property_value_finder.rb b/nuget/lib/dependabot/nuget/file_parser/property_value_finder.rb deleted file mode 100644 index 72c93739105..00000000000 --- a/nuget/lib/dependabot/nuget/file_parser/property_value_finder.rb +++ /dev/null @@ -1,223 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "dependabot/nuget/file_fetcher/import_paths_finder" -require "dependabot/nuget/file_parser" - -# For docs, see: -# - https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-properties -# - https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build -module Dependabot - module Nuget - class FileParser - class PropertyValueFinder - extend T::Sig - - PROPERTY_REGEX = /\$\((?.*?)\)/ - - sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void } - def initialize(dependency_files:) - @dependency_files = dependency_files - end - - sig do - params(property_name: String, - callsite_file: Dependabot::DependencyFile, - stack: T::Array[[String, String]]) - .returns(T.nilable(T::Hash[T.untyped, T.untyped])) - end - def property_details(property_name:, callsite_file:, stack: []) - stack += [[property_name, callsite_file.name]] - return if property_name.include?("(") - - node_details = deep_find_prop_node( - property: property_name, - file: callsite_file - ) - - node_details ||= - find_property_in_directory_build_targets( - property: property_name, - callsite_file: callsite_file - ) - - node_details ||= - find_property_in_directory_build_props( - property: property_name, - callsite_file: callsite_file - ) - - node_details ||= - find_property_in_directory_packages_props( - property: property_name, - callsite_file: callsite_file - ) - - node_details ||= - find_property_in_packages_props(property: property_name) - - return unless node_details - return node_details unless PROPERTY_REGEX.match?(node_details[:value]) - - check_next_level_of_stack(node_details, stack) - end - - sig do - params(node_details: T.untyped, - stack: T::Array[[String, String]]) - .returns(T.nilable(T::Hash[T.untyped, T.untyped])) - end - def check_next_level_of_stack(node_details, stack) - property_name = node_details.fetch(:value) - .match(PROPERTY_REGEX) - .named_captures.fetch("property") - callsite_file = dependency_files - .find { |f| f.name == node_details.fetch(:file) } - return unless callsite_file - - raise "Circular reference!" if stack.include?([property_name, callsite_file.name]) - - property_details( - property_name: property_name, - callsite_file: callsite_file, - stack: stack - ) - end - - private - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :dependency_files - - sig do - params(property: String, - file: Dependabot::DependencyFile) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def deep_find_prop_node(property:, file:) - doc = Nokogiri::XML(file.content) - doc.remove_namespaces! - node = doc.at_xpath(property_xpath(property)) - - # If we found a value for the property, return it - return node_details(file: file, node: node, property: property) if node - - # Otherwise, we need to look in an imported file - import_path_finder = - Nuget::FileFetcher::ImportPathsFinder - .new(project_file: file) - - import_paths = [ - *import_path_finder.import_paths, - *import_path_finder.project_reference_paths - ] - - file = import_paths - .filter_map { |p| dependency_files.find { |f| f.name == p } } - .find { |f| deep_find_prop_node(property: property, file: f) } - - return unless file - - deep_find_prop_node(property: property, file: file) - end - - sig do - params(property: String, callsite_file: Dependabot::DependencyFile) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_property_in_directory_build_targets(property:, callsite_file:) - find_property_in_up_tree_files(property: property, callsite_file: callsite_file, - expected_file_name: "Directory.Build.targets") - end - - sig do - params(property: String, callsite_file: Dependabot::DependencyFile) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_property_in_directory_build_props(property:, callsite_file:) - find_property_in_up_tree_files(property: property, callsite_file: callsite_file, - expected_file_name: "Directory.Build.props") - end - - sig do - params(property: String, callsite_file: Dependabot::DependencyFile) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_property_in_directory_packages_props(property:, callsite_file:) - find_property_in_up_tree_files(property: property, callsite_file: callsite_file, - expected_file_name: "Directory.Packages.props") - end - - sig { params(property: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def find_property_in_packages_props(property:) - file = packages_props_file - return unless file - - deep_find_prop_node(property: property, file: file) - end - - sig do - params(property: String, - callsite_file: Dependabot::DependencyFile, - expected_file_name: String) - .returns(T.untyped) - end - def find_property_in_up_tree_files(property:, callsite_file:, expected_file_name:) - files = up_tree_files_for_project(callsite_file, expected_file_name) - return if files.empty? - - # first file where we were able to find the node - files.reduce(T.let(nil, T.nilable(String))) do |acc, file| - acc || deep_find_prop_node(property: property, file: file) - end - end - - sig do - params(project_file: DependencyFile, expected_file_name: String).returns(T::Array[Dependabot::DependencyFile]) - end - def up_tree_files_for_project(project_file, expected_file_name) - dir = File.dirname(project_file.name) - - # Simulate MSBuild walking up the directory structure looking for a file - possible_paths = dir.split("/").map.with_index do |_, i| - base = dir.split("/").first(i + 1).join("/") - Pathname.new(base + "/#{expected_file_name}").cleanpath.to_path - end.reverse + [expected_file_name] - - paths = - possible_paths.uniq - .select { |p| dependency_files.find { |f| f.name.casecmp(p)&.zero? } } - - dependency_files.select { |f| paths.include?(f.name) } - end - - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def packages_props_file - dependency_files.find { |f| f.name.casecmp("Packages.props")&.zero? } - end - - sig { params(property_name: String).returns(String) } - def property_xpath(property_name) - # only return properties that don't have a `Condition` attribute or the `Condition` attribute is checking for - # an empty string, e.g., Condition="$(SomeProperty) == ''" - %{/Project/PropertyGroup/#{property_name}[not(@Condition) or @Condition="$(#{property_name}) == ''"]} - end - - sig do - params(file: DependencyFile, - node: Nokogiri::XML::Node, - property: String) - .returns(T::Hash[Symbol, T.untyped]) - end - def node_details(file:, node:, property:) - { - file: file.name, - node: node, - value: node.content.strip, - root_property_name: property - } - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/file_updater.rb b/nuget/lib/dependabot/nuget/file_updater.rb index ec383448a1b..cd9bf545fc3 100644 --- a/nuget/lib/dependabot/nuget/file_updater.rb +++ b/nuget/lib/dependabot/nuget/file_updater.rb @@ -4,6 +4,9 @@ require "dependabot/dependency_file" require "dependabot/file_updaters" require "dependabot/file_updaters/base" +require "dependabot/nuget/discovery/dependency_details" +require "dependabot/nuget/discovery/discovery_json_reader" +require "dependabot/nuget/discovery/workspace_discovery" require "dependabot/nuget/native_helpers" require "dependabot/shared_helpers" require "sorbet-runtime" @@ -13,11 +16,6 @@ module Nuget class FileUpdater < Dependabot::FileUpdaters::Base extend T::Sig - require_relative "file_updater/property_value_updater" - require_relative "file_parser/project_file_parser" - require_relative "file_parser/dotnet_tools_json_parser" - require_relative "file_parser/packages_config_parser" - sig { override.returns(T::Array[Regexp]) } def self.updated_files_regex [ @@ -67,7 +65,7 @@ def try_update_projects(dependency) project_dependencies = project_dependencies(project_file) proj_path = dependency_file_path(project_file) - next unless project_dependencies.any? { |dep| dep.name.casecmp(dependency.name)&.zero? } + next unless project_dependencies.any? { |dep| dep.name.casecmp?(dependency.name) } next unless repo_contents_path @@ -76,7 +74,7 @@ def try_update_projects(dependency) checked_files.add(checked_key) # We need to check the downstream references even though we're already evaluated the file - downstream_files = project_file_parser.downstream_file_references(project_file: project_file) + downstream_files = referenced_project_paths(project_file) downstream_files.each do |downstream_file| checked_files.add("#{downstream_file}-#{dependency.name}#{dependency.version}") end @@ -87,8 +85,8 @@ def try_update_projects(dependency) sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def try_update_json(dependency) - if dotnet_tools_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name)&.zero? } || - global_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name)&.zero? } + if dotnet_tools_json_dependencies.any? { |dep| dep.name.casecmp?(dependency.name) } || + global_json_dependencies.any? { |dep| dep.name.casecmp?(dependency.name) } # We just need to feed the updater a project file, grab the first project_file = T.must(project_files.first) @@ -128,58 +126,38 @@ def testonly_update_tooling_calls @update_tooling_calls end - sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[Dependabot::Dependency]) } - def project_dependencies(project_file) - # Collect all dependencies from the project file and associated packages.config - dependencies = project_file_parser.dependency_set(project_file: project_file).dependencies - packages_config = find_packages_config(project_file) - return dependencies unless packages_config + sig { returns(T.nilable(WorkspaceDiscovery)) } + def workspace + @workspace ||= T.let(begin + discovery_json = DiscoveryJsonReader.discovery_json + if discovery_json + workspace = DiscoveryJsonReader.new( + discovery_json: discovery_json + ).workspace_discovery + end - dependencies + FileParser::PackagesConfigParser.new(packages_config: packages_config) - .dependency_set.dependencies + workspace + end, T.nilable(WorkspaceDiscovery)) end - sig { params(project_file: Dependabot::DependencyFile).returns(T.nilable(Dependabot::DependencyFile)) } - def find_packages_config(project_file) - project_file_name = File.basename(project_file.name) - packages_config_path = project_file.name.gsub(project_file_name, "packages.config") - packages_config_files.find { |f| f.name == packages_config_path } + sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[String]) } + def referenced_project_paths(project_file) + workspace&.projects&.find { |p| p.file_path == project_file.name }&.referenced_project_paths || [] end - sig { returns(Dependabot::Nuget::FileParser::ProjectFileParser) } - def project_file_parser - @project_file_parser ||= - T.let( - FileParser::ProjectFileParser.new( - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: repo_contents_path - ), - T.nilable(Dependabot::Nuget::FileParser::ProjectFileParser) - ) + sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[DependencyDetails]) } + def project_dependencies(project_file) + workspace&.projects&.find { |p| p.file_path == project_file.name }&.dependencies || [] end - sig { returns(T::Array[Dependabot::Dependency]) } + sig { returns(T::Array[DependencyDetails]) } def global_json_dependencies - return [] unless global_json - - @global_json_dependencies ||= - T.let( - FileParser::GlobalJsonParser.new(global_json: T.must(global_json)).dependency_set.dependencies, - T.nilable(T::Array[Dependabot::Dependency]) - ) + workspace&.global_json&.dependencies || [] end - sig { returns(T::Array[Dependabot::Dependency]) } + sig { returns(T::Array[DependencyDetails]) } def dotnet_tools_json_dependencies - return [] unless dotnet_tools_json - - @dotnet_tools_json_dependencies ||= - T.let( - FileParser::DotNetToolsJsonParser.new(dotnet_tools_json: T.must(dotnet_tools_json)) - .dependency_set.dependencies, - T.nilable(T::Array[Dependabot::Dependency]) - ) + workspace&.dotnet_tools_json&.dependencies || [] end # rubocop:disable Metrics/PerceivedComplexity @@ -234,16 +212,6 @@ def packages_config_files end end - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def global_json - dependency_files.find { |f| T.must(f.name.casecmp("global.json")).zero? } - end - - sig { returns(T.nilable(Dependabot::DependencyFile)) } - def dotnet_tools_json - dependency_files.find { |f| T.must(f.name.casecmp(".config/dotnet-tools.json")).zero? } - end - sig { override.void } def check_required_files return if project_files.any? || packages_config_files.any? diff --git a/nuget/lib/dependabot/nuget/file_updater/property_value_updater.rb b/nuget/lib/dependabot/nuget/file_updater/property_value_updater.rb deleted file mode 100644 index da4fb4fc698..00000000000 --- a/nuget/lib/dependabot/nuget/file_updater/property_value_updater.rb +++ /dev/null @@ -1,80 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" - -require "dependabot/dependency_file" -require "dependabot/nuget/file_updater" -require "dependabot/nuget/file_parser/property_value_finder" - -module Dependabot - module Nuget - class FileUpdater - class PropertyValueUpdater - extend T::Sig - - sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void } - def initialize(dependency_files:) - @dependency_files = dependency_files - end - - sig do - params(property_name: String, - updated_value: String, - callsite_file: Dependabot::DependencyFile) - .returns(T::Array[Dependabot::DependencyFile]) - end - def update_files_for_property_change(property_name:, updated_value:, - callsite_file:) - declaration_details = - property_value_finder.property_details( - property_name: property_name, - callsite_file: callsite_file - ) - throw "Unable to locate property details" unless declaration_details - - declaration_filename = declaration_details.fetch(:file) - declaration_file = dependency_files.find do |f| - declaration_filename == f.name - end - throw "Unable to locate declaration file" unless declaration_file - - content = T.must(declaration_file.content) - node = declaration_details.fetch(:node) - - updated_content = content.sub( - %r{(<#{Regexp.quote(node.name)}(?:\s[^>]*)?>) - \s*#{Regexp.quote(node.content)}\s* - }xm, - '\1' + "#{updated_value}" - ) - - files = dependency_files.dup - file_index = T.must(files.index(declaration_file)) - files[file_index] = - update_file(file: declaration_file, content: updated_content) - files - end - - private - - sig { returns(T::Array[DependencyFile]) } - attr_reader :dependency_files - - sig { returns(FileParser::PropertyValueFinder) } - def property_value_finder - @property_value_finder ||= - T.let(FileParser::PropertyValueFinder - .new(dependency_files: dependency_files), T.nilable(FileParser::PropertyValueFinder)) - end - - sig { params(file: DependencyFile, content: String).returns(DependencyFile) } - def update_file(file:, content:) - updated_file = file.dup - updated_file.content = content - updated_file - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_helpers.rb b/nuget/lib/dependabot/nuget/native_helpers.rb index cab57ea2288..4047feb226d 100644 --- a/nuget/lib/dependabot/nuget/native_helpers.rb +++ b/nuget/lib/dependabot/nuget/native_helpers.rb @@ -55,6 +55,61 @@ def self.run_nuget_framework_check(project_tfms, package_tfms) false end + sig do + params(repo_root: String, workspace_path: String, output_path: String).returns([String, String]) + end + def self.get_nuget_discover_tool_command(repo_root:, workspace_path:, output_path:) + exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli") + command_parts = [ + exe_path, + "discover", + "--repo-root", + repo_root, + "--workspace", + workspace_path, + "--output", + output_path, + "--verbose" + ].compact + + command = Shellwords.join(command_parts) + + fingerprint = [ + exe_path, + "discover", + "--repo-root", + "", + "--workspace", + "", + "--output", + "", + "--verbose" + ].compact.join(" ") + + [command, fingerprint] + end + + sig do + params( + repo_root: String, + workspace_path: String, + output_path: String, + credentials: T::Array[Dependabot::Credential] + ).void + end + def self.run_nuget_discover_tool(repo_root:, workspace_path:, output_path:, credentials:) + (command, fingerprint) = get_nuget_discover_tool_command(repo_root: repo_root, + workspace_path: workspace_path, + output_path: output_path) + + puts "running NuGet discovery:\n" + command + + NuGetConfigCredentialHelpers.patch_nuget_config_for_action(credentials) do + output = SharedHelpers.run_shell_command(command, allow_unsafe_shell_command: true, fingerprint: fingerprint) + puts output + end + end + sig do params(repo_root: String, proj_path: String, dependency: Dependency, is_transitive: T::Boolean).returns([String, String]) diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index b553b641dfd..a6e4f49e55a 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -16,6 +16,8 @@ class UpdateChecker < Dependabot::UpdateCheckers::Base require_relative "update_checker/requirements_updater" require_relative "update_checker/dependency_finder" + PROPERTY_REGEX = /\$\((?.*?)\)/ + sig { override.returns(T.nilable(String)) } def latest_version # No need to find latest version for transitive dependencies unless they have a vulnerability. @@ -81,7 +83,7 @@ def requirements_unlocked_or_can_be? # that property couldn't be found, and the requirement therefore # cannot be unlocked (since we can't update that property) dependency.requirements.none? do |req| - req.fetch(:requirement)&.match?(Nuget::FileParser::PropertyValueFinder::PROPERTY_REGEX) + req.fetch(:requirement)&.match?(PROPERTY_REGEX) end end @@ -219,6 +221,7 @@ def all_property_based_dependencies T.let( Nuget::FileParser.new( dependency_files: dependency_files, + repo_contents_path: repo_contents_path, source: nil ).parse.select do |dep| dep.requirements.any? { |req| req.dig(:metadata, :property_name) } diff --git a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb b/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb index 9a813d6aa1f..c2d3aa1699b 100644 --- a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb @@ -11,10 +11,9 @@ class CompatibilityChecker require_relative "tfm_finder" require_relative "tfm_comparer" - def initialize(dependency_urls:, dependency:, tfm_finder:) + def initialize(dependency_urls:, dependency:) @dependency_urls = dependency_urls @dependency = dependency - @tfm_finder = tfm_finder end def compatible?(version) @@ -39,7 +38,7 @@ def compatible?(version) private - attr_reader :dependency_urls, :dependency, :tfm_finder + attr_reader :dependency_urls, :dependency def pure_development_dependency?(nuspec_xml) contents = nuspec_xml.at_xpath("package/metadata/developmentDependency")&.content&.strip @@ -62,7 +61,7 @@ def parse_package_tfms(nuspec_xml) def project_tfms return @project_tfms if defined?(@project_tfms) - @project_tfms = tfm_finder.frameworks(dependency) + @project_tfms = TfmFinder.frameworks(dependency) end def fetch_package_tfms(dependency_version) diff --git a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb b/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb index 525b172b8da..4d525dd5e46 100644 --- a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb @@ -121,6 +121,7 @@ def top_level_dependencies @top_level_dependencies ||= Nuget::FileParser.new( dependency_files: dependency_files, + repo_contents_path: repo_contents_path, source: nil ).parse.select(&:top_level?) end diff --git a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb b/nuget/lib/dependabot/nuget/update_checker/property_updater.rb index f589390df48..ed3b12062a9 100644 --- a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb +++ b/nuget/lib/dependabot/nuget/update_checker/property_updater.rb @@ -97,6 +97,7 @@ def dependencies_using_property @dependencies_using_property ||= Nuget::FileParser.new( dependency_files: dependency_files, + repo_contents_path: repo_contents_path, source: nil ).parse.select do |dep| dep.requirements.any? do |r| diff --git a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb b/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb index 392fbe0ce26..04c5d074892 100644 --- a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb @@ -1,126 +1,26 @@ -# typed: true +# typed: strong # frozen_string_literal: true -require "excon" -require "nokogiri" - -require "dependabot/update_checkers/base" -require "dependabot/nuget/version" -require "dependabot/nuget/requirement" -require "dependabot/nuget/native_helpers" -require "dependabot/shared_helpers" +require "dependabot/nuget/discovery/discovery_json_reader" module Dependabot module Nuget class TfmFinder - require "dependabot/nuget/file_parser/packages_config_parser" - require "dependabot/nuget/file_parser/project_file_parser" - - def initialize(dependency_files:, credentials:, repo_contents_path:) - @dependency_files = dependency_files - @credentials = credentials - @repo_contents_path = repo_contents_path - end - - def frameworks(dependency) - tfms = Set.new - tfms += project_file_tfms(dependency) - tfms += project_import_file_tfms - tfms.to_a - end - - private - - attr_reader :dependency_files, :credentials, :repo_contents_path - - def project_file_tfms(dependency) - project_files_with_dependency(dependency).flat_map do |file| - project_file_parser.target_frameworks(project_file: file) - end - end - - def project_files_with_dependency(dependency) - project_files.select do |file| - packages_config_contains_dependency?(file, dependency) || - project_file_contains_dependency?(file, dependency) - end - end - - def packages_config_contains_dependency?(file, dependency) - config_file = find_packages_config_file(file) - return false unless config_file - - config_parser = FileParser::PackagesConfigParser.new(packages_config: config_file) - config_parser.dependency_set.dependencies.any? do |d| - d.name.casecmp(dependency.name)&.zero? - end - end - - def project_file_contains_dependency?(file, dependency) - project_file_parser.dependency_set(project_file: file).dependencies.any? do |d| - d.name.casecmp(dependency.name)&.zero? - end - end - - def find_packages_config_file(file) - return file if file.name.end_with?("packages.config") + extend T::Sig - filename = File.basename(file.name) - search_path = file.name.sub(filename, "packages.config") + sig { params(dependency: Dependency).returns(T::Array[String]) } + def self.frameworks(dependency) + discovery_json = DiscoveryJsonReader.discovery_json + return [] unless discovery_json - dependency_files.find { |f| f.name.casecmp(search_path).zero? } - end - - def project_import_file_tfms - @project_import_file_tfms ||= project_import_files.flat_map do |file| - project_file_parser.target_frameworks(project_file: file) - end - end - - def project_file_parser - @project_file_parser ||= - FileParser::ProjectFileParser.new( - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: repo_contents_path - ) - end - - def project_files - projfile = /\.[a-z]{2}proj$/ - packageprops = /[Dd]irectory.[Pp]ackages.props/ - - dependency_files.select do |df| - df.name.match?(projfile) || - df.name.match?(packageprops) - end - end - - def packages_config_files - dependency_files.select do |f| - f.name.split("/").last.casecmp("packages.config").zero? - end - end - - def project_import_files - dependency_files - - project_files - - packages_config_files - - nuget_configs - - [global_json] - - [dotnet_tools_json] - end - - def nuget_configs - dependency_files.select { |f| f.name.match?(/nuget\.config$/i) } - end - - def global_json - dependency_files.find { |f| f.name.casecmp("global.json").zero? } - end + workspace = DiscoveryJsonReader.new( + discovery_json: discovery_json + ).workspace_discovery + return [] unless workspace - def dotnet_tools_json - dependency_files.find { |f| f.name.casecmp(".config/dotnet-tools.json").zero? } + workspace.projects.select do |project| + project.dependencies.any? { |d| d.name.casecmp?(dependency.name) } + end.flat_map(&:target_frameworks).uniq end end end diff --git a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb b/nuget/lib/dependabot/nuget/update_checker/version_finder.rb index a923a6dae08..8b66eff3d1b 100644 --- a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/version_finder.rb @@ -160,12 +160,7 @@ def compatibility_checker T.let( CompatibilityChecker.new( dependency_urls: dependency_urls, - dependency: dependency, - tfm_finder: TfmFinder.new( - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: repo_contents_path - ) + dependency: dependency ), T.nilable(Dependabot::Nuget::CompatibilityChecker) ) diff --git a/nuget/spec/dependabot/nuget/file_updater/property_value_updater_spec.rb b/nuget/spec/dependabot/nuget/file_updater/property_value_updater_spec.rb deleted file mode 100644 index 2effad33408..00000000000 --- a/nuget/spec/dependabot/nuget/file_updater/property_value_updater_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency_file" -require "dependabot/nuget/file_updater/property_value_updater" - -RSpec.describe Dependabot::Nuget::FileUpdater::PropertyValueUpdater do - let(:updater) { described_class.new(dependency_files: files) } - let(:files) { [project_file] } - - let(:project_file) do - Dependabot::DependencyFile.new( - name: "my.csproj", - content: fixture("csproj", csproj_fixture_name) - ) - end - let(:csproj_fixture_name) { "property_version.csproj" } - - describe "#update_files_for_property_change" do - subject(:updated_files) do - updater.update_files_for_property_change( - property_name: property_name, - updated_value: updated_value, - callsite_file: project_file - ) - end - let(:property_name) { "NukeVersion" } - let(:updated_value) { "0.1.500" } - - it "updates the property" do - expect(updated_files.first.content) - .to include( - %(0.1.500) - ) - expect(updated_files.first.content) - .to include('Version="$(NukeVersion)" />') - end - - context "when the property is inherited" do - let(:files) { [project_file, build_file, imported_file] } - - let(:project_file) do - Dependabot::DependencyFile.new( - name: "nested/my.csproj", - content: file_body - ) - end - let(:file_body) { fixture("csproj", "property_version.csproj") } - let(:build_file) do - Dependabot::DependencyFile.new( - name: "Directory.Build.props", - content: build_file_body - ) - end - let(:build_file_body) { fixture("property_files", "imports") } - let(:imported_file) do - Dependabot::DependencyFile.new( - name: "build/dependencies.props", - content: imported_file_body - ) - end - let(:imported_file_body) do - fixture("property_files", "dependency.props") - end - - let(:property_name) { "XunitPackageVersion" } - - it "updates the property" do - expect(updated_files.count).to eq(3) - - changed_files = updated_files - files - expect(changed_files.count).to eq(1) - - changed_file = changed_files.first - - expect(changed_file.name).to eq("build/dependencies.props") - expect(changed_file.content) - .to include("0.1.500") - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb index b0dc1fd2e74..ad6915841f7 100644 --- a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb @@ -11,8 +11,7 @@ subject(:checker) do described_class.new( dependency_urls: dependency_urls, - dependency: dependency, - tfm_finder: tfm_finder + dependency: dependency ) end @@ -47,14 +46,6 @@ [{ file: "my.csproj", requirement: "5.0.2", groups: ["dependencies"], source: nil }] end - let(:tfm_finder) do - Dependabot::Nuget::TfmFinder.new( - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: "test/repo" - ) - end - let(:dependency_files) { [csproj] } let(:csproj) do Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) diff --git a/nuget/spec/spec_helper.rb b/nuget/spec/spec_helper.rb index 942321ddaf7..a23c3fa17f4 100644 --- a/nuget/spec/spec_helper.rb +++ b/nuget/spec/spec_helper.rb @@ -1,6 +1,7 @@ # typed: true # frozen_string_literal: true +ENV["DEPENDABOT_NUGET_TEST_RUN"] = "true" ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" def common_dir From 962f70cac588a524e5667be457857fce5c955a8f Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Thu, 14 Mar 2024 00:17:03 -0700 Subject: [PATCH 10/26] Fix typo --- .../NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs index c709faba165..c3c371937c2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs @@ -14,7 +14,7 @@ internal static class DiscoverCommand internal static Command GetCommand(Action setExitCode) { - Command command = new("discover", "Generates a report of the workspace depenedencies and where they are located.") + Command command = new("discover", "Generates a report of the workspace dependencies and where they are located.") { RepoRootOption, WorkspaceOption, From 7c565806abe1631e9fcbed548531c0913184d940 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Thu, 14 Mar 2024 12:50:56 -0700 Subject: [PATCH 11/26] PR Feedback --- .../Utilities/AssertEx.cs | 29 +++++++++---------- .../Discover/DiscoveryWorker.cs | 15 +++++++--- .../Discover/PackagesConfigDiscovery.cs | 2 +- .../Updater/UpdaterWorker.cs | 2 +- .../Utilities/NuGetHelper.cs | 11 +++++-- .../nuget/discovery/dependency_details.rb | 1 + .../discovery/dependency_file_discovery.rb | 1 + .../directory_packages_props_discovery.rb | 1 + .../nuget/discovery/discovery_json_reader.rb | 4 +-- .../nuget/discovery/evaluation_details.rb | 2 ++ .../nuget/discovery/project_discovery.rb | 1 + .../nuget/discovery/property_details.rb | 2 ++ .../nuget/discovery/workspace_discovery.rb | 1 + 13 files changed, 47 insertions(+), 25 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs index 033c844f9ea..84f093fd318 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections; using System.Collections.Immutable; using System.Reflection; @@ -17,8 +15,8 @@ public static class AssertEx public static void Equal( ImmutableArray expected, ImmutableArray actual, - IEqualityComparer comparer = null, - string message = null) + IEqualityComparer? comparer = null, + string? message = null) { Equal(expected, (IEnumerable)actual, comparer, message); } @@ -26,8 +24,8 @@ public static void Equal( public static void Equal( ImmutableArray expected, IEnumerable actual, - IEqualityComparer comparer = null, - string message = null) + IEqualityComparer? comparer = null, + string? message = null) { if (actual == null || expected.IsDefault) { @@ -42,8 +40,8 @@ public static void Equal( public static void Equal( IEnumerable expected, ImmutableArray actual, - IEqualityComparer comparer = null, - string message = null) + IEqualityComparer? comparer = null, + string? message = null) { if (expected == null || actual.IsDefault) { @@ -58,12 +56,13 @@ public static void Equal( public static void Equal( IEnumerable expected, IEnumerable actual, - IEqualityComparer comparer = null, - string message = null) + IEqualityComparer? comparer = null, + string? message = null) { if (expected == null) { Assert.Null(actual); + return; } else { @@ -81,7 +80,7 @@ public static void Equal( private static bool SequenceEqual( IEnumerable expected, IEnumerable actual, - IEqualityComparer comparer = null) + IEqualityComparer? comparer = null) { if (ReferenceEquals(expected, actual)) { @@ -124,12 +123,12 @@ private static bool SequenceEqual( public static string GetAssertMessage( IEnumerable expected, IEnumerable actual, - IEqualityComparer comparer = null, - string prefix = null) + IEqualityComparer? comparer = null, + string? prefix = null) { Func itemInspector = typeof(T) == typeof(byte) ? b => $"0x{b:X2}" - : new Func(obj => (obj != null) ? obj.ToString() : ""); + : new Func(obj => obj?.ToString() ?? ""); var itemSeparator = typeof(T) == typeof(byte) ? ", " @@ -187,7 +186,7 @@ public static bool Equals(T left, T right) return Instance.Equals(left, right); } - bool IEqualityComparer.Equals(T x, T y) + bool IEqualityComparer.Equals(T? x, T? y) { if (CanBeNull()) { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index 427f121d18e..abfbbf03a79 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -61,11 +61,15 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out break; } } - else + else if (Directory.Exists(workspacePath)) { workspaceType = WorkspaceType.Directory; projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath); } + else + { + _logger.Log($"Workspace path [{workspacePath}] does not exist."); + } var directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger); @@ -105,9 +109,12 @@ private async Task> RunForDirectoryAsnyc( private static ImmutableArray FindProjectFiles(string workspacePath) { - return Directory.EnumerateFiles(workspacePath, "*.csproj", SearchOption.AllDirectories) - .Concat(Directory.EnumerateFiles(workspacePath, "*.vbproj", SearchOption.AllDirectories)) - .Concat(Directory.EnumerateFiles(workspacePath, "*.fsproj", SearchOption.AllDirectories)) + return Directory.EnumerateFiles(workspacePath, "*.??proj", SearchOption.AllDirectories) + .Where(path => + { + var extension = Path.GetExtension(path).ToLowerInvariant(); + return extension == ".csproj" || extension == ".fsproj" || extension == ".vbproj"; + }) .ToImmutableArray(); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs index b31eda825b7..31871f0e111 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs @@ -26,7 +26,7 @@ internal static class PackagesConfigDiscovery private static PackagesConfigBuildFile? TryLoadBuildFile(string repoRootPath, string projectPath, Logger logger) { - return NuGetHelper.HasPackagesConfigFile(projectPath, out var packagesConfigPath) + return NuGetHelper.TryGetPackagesConfigFile(projectPath, out var packagesConfigPath) ? PackagesConfigBuildFile.Open(repoRootPath, packagesConfigPath) : null; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs index 3b75334d5ae..a47ca0b870a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs @@ -134,7 +134,7 @@ private async Task RunUpdaterAsync( _logger.Log($"Updating project [{projectPath}]"); - if (NuGetHelper.HasPackagesConfigFile(projectPath, out _)) + if (NuGetHelper.TryGetPackagesConfigFile(projectPath, out _)) { await PackagesConfigUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, _logger); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs index 06abdca17f3..e971d0f6f05 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs @@ -6,10 +6,17 @@ internal static class NuGetHelper { internal const string PackagesConfigFileName = "packages.config"; - public static bool HasPackagesConfigFile(string projectPath, [NotNullWhen(returnValue: true)] out string? packagesConfigPath) + public static bool TryGetPackagesConfigFile(string projectPath, [NotNullWhen(returnValue: true)] out string? packagesConfigPath) { var projectDirectory = Path.GetDirectoryName(projectPath); + packagesConfigPath = PathHelper.JoinPath(projectDirectory, PackagesConfigFileName); - return File.Exists(packagesConfigPath); + if (File.Exists(packagesConfigPath)) + { + return true; + } + + packagesConfigPath = null; + return false; } } diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_details.rb b/nuget/lib/dependabot/nuget/discovery/dependency_details.rb index f2ddde19c1a..6a72f4aa3f2 100644 --- a/nuget/lib/dependabot/nuget/discovery/dependency_details.rb +++ b/nuget/lib/dependabot/nuget/discovery/dependency_details.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "dependabot/nuget/discovery/evaluation_details" +require "sorbet-runtime" module Dependabot module Nuget diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb index e92f79390f1..b6798a2f6be 100644 --- a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "dependabot/nuget/discovery/dependency_details" +require "sorbet-runtime" module Dependabot module Nuget diff --git a/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb b/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb index 52a4fa43a92..b48d65557a3 100644 --- a/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "dependabot/nuget/discovery/dependency_details" +require "sorbet-runtime" module Dependabot module Nuget diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index e6889b1c80c..4e39cd63455 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -1,9 +1,9 @@ # typed: strong # frozen_string_literal: true -require "json" - require "dependabot/dependency" +require "json" +require "sorbet-runtime" module Dependabot module Nuget diff --git a/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb b/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb index ddac1c99f08..2c1f0d50d98 100644 --- a/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb +++ b/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb @@ -1,6 +1,8 @@ # typed: strong # frozen_string_literal: true +require "sorbet-runtime" + module Dependabot module Nuget class EvaluationDetails diff --git a/nuget/lib/dependabot/nuget/discovery/project_discovery.rb b/nuget/lib/dependabot/nuget/discovery/project_discovery.rb index 66cc9e46579..fef9c73821e 100644 --- a/nuget/lib/dependabot/nuget/discovery/project_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/project_discovery.rb @@ -3,6 +3,7 @@ require "dependabot/nuget/discovery/dependency_details" require "dependabot/nuget/discovery/property_details" +require "sorbet-runtime" module Dependabot module Nuget diff --git a/nuget/lib/dependabot/nuget/discovery/property_details.rb b/nuget/lib/dependabot/nuget/discovery/property_details.rb index 9daa7e8bb24..1ac2ac43578 100644 --- a/nuget/lib/dependabot/nuget/discovery/property_details.rb +++ b/nuget/lib/dependabot/nuget/discovery/property_details.rb @@ -1,6 +1,8 @@ # typed: strong # frozen_string_literal: true +require "sorbet-runtime" + module Dependabot module Nuget class PropertyDetails diff --git a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb index 4c5b6ed2189..8c0086f65d1 100644 --- a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb @@ -4,6 +4,7 @@ require "dependabot/nuget/discovery/dependency_file_discovery" require "dependabot/nuget/discovery/directory_packages_props_discovery" require "dependabot/nuget/discovery/project_discovery" +require "sorbet-runtime" module Dependabot module Nuget From 738d2116445b7f7cfe18859a2d9a0e563f8a9d88 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Thu, 14 Mar 2024 21:41:50 -0700 Subject: [PATCH 12/26] Returns paths which are relative to the target folder. --- .../lib/NuGetUpdater/Directory.Packages.props | 2 - .../EntryPointTests.Discover.cs | 17 +-- .../Discover/DiscoveryWorkerTestBase.cs | 2 - .../Discover/DiscoveryWorkerTests.cs | 28 ++--- .../Discover/ExpectedDiscoveryResults.cs | 2 - .../Files/DotNetToolsJsonBuildFileTests.cs | 3 +- .../Files/GlobalJsonBuildFileTests.cs | 4 +- .../Files/PackagesConfigBuildFileTests.cs | 4 +- .../Files/ProjectBuildFileTests.cs | 4 +- .../Utilities/SdkPackageUpdaterHelperTests.cs | 2 +- .../DirectoryPackagesPropsDiscovery.cs | 15 ++- .../Discover/DiscoveryWorker.cs | 102 ++++-------------- .../Discover/DotNetToolsJsonDiscovery.cs | 16 +-- .../Discover/GlobalJsonDiscovery.cs | 16 +-- .../Discover/PackagesConfigDiscovery.cs | 18 ++-- .../Discover/SdkProjectDiscovery.cs | 6 +- .../Discover/WorkspaceDiscoveryResult.cs | 11 -- .../NuGetUpdater.Core/Files/BuildFile.cs | 12 +-- .../Files/DotNetToolsJsonBuildFile.cs | 11 +- .../Files/GlobalJsonBuildFile.cs | 27 ++--- .../NuGetUpdater.Core/Files/JsonBuildFile.cs | 3 +- .../Files/PackagesConfigBuildFile.cs | 17 ++- .../Files/ProjectBuildFile.cs | 12 +-- .../NuGetUpdater.Core/Files/XmlBuildFile.cs | 4 +- .../Updater/DotNetToolsJsonUpdater.cs | 23 ++-- .../Updater/GlobalJsonUpdater.cs | 20 +--- .../Updater/SdkPackageUpdater.cs | 26 ++--- .../Utilities/MSBuildHelper.cs | 23 ++-- .../nuget/discovery/workspace_discovery.rb | 17 +-- 29 files changed, 139 insertions(+), 308 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/Directory.Packages.props b/nuget/helpers/lib/NuGetUpdater/Directory.Packages.props index ccb4ce00616..836db8d9255 100644 --- a/nuget/helpers/lib/NuGetUpdater/Directory.Packages.props +++ b/nuget/helpers/lib/NuGetUpdater/Directory.Packages.props @@ -9,8 +9,6 @@ - - diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index 75f4f5b441e..34d5df6d762 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -2,7 +2,6 @@ using System.Text; using NuGetUpdater.Core; -using NuGetUpdater.Core.Discover; using NuGetUpdater.Core.Test.Discover; using Xunit; @@ -25,7 +24,7 @@ await RunAsync(path => "--repo-root", path, "--workspace", - Path.Combine(path, solutionPath), + path, ], new[] { @@ -78,9 +77,7 @@ await RunAsync(path => }, expectedResult: new() { - FilePath = solutionPath, - Type = WorkspaceType.Solution, - TargetFrameworks = ["net45"], + FilePath = "", Projects = [ new() { @@ -110,7 +107,7 @@ await RunAsync(path => "--repo-root", path, "--workspace", - Path.Combine(path, projectPath), + path, ], new[] { @@ -140,9 +137,7 @@ await RunAsync(path => }, expectedResult: new() { - FilePath = projectPath, - Type = WorkspaceType.Project, - TargetFrameworks = ["net45"], + FilePath = "", Projects = [ new() { @@ -203,12 +198,10 @@ await RunAsync(path => expectedResult: new() { FilePath = workspacePath, - Type = WorkspaceType.Directory, - TargetFrameworks = ["net45"], Projects = [ new() { - FilePath = "path/to/my.csproj", + FilePath = "my.csproj", TargetFrameworks = ["net45"], ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index 22125c03b24..d0ad18c8750 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -30,8 +30,6 @@ protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult e { Assert.NotNull(actualResult); Assert.Equal(expectedResult.FilePath, actualResult.FilePath); - Assert.Equal(expectedResult.Type, actualResult.Type); - AssertEx.Equal(expectedResult.TargetFrameworks, actualResult.TargetFrameworks); ValidateDirectoryPackagesProps(expectedResult.DirectoryPackagesProps, actualResult.DirectoryPackagesProps); ValidateResultWithDependencies(expectedResult.GlobalJson, actualResult.GlobalJson); ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs index 1dc11229369..7ee55499e87 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -1,7 +1,5 @@ using System.Collections.Immutable; -using NuGetUpdater.Core.Discover; - using Xunit; namespace NuGetUpdater.Core.Test.Discover; @@ -15,7 +13,7 @@ public class DiscoveryWorkerTests : DiscoveryWorkerTestBase public async Task TestProjectFiles(string projectPath) { await TestDiscovery( - workspacePath: projectPath, + workspacePath: "src", files: new[] { (projectPath, """ @@ -33,13 +31,11 @@ await TestDiscovery( }, expectedResult: new() { - FilePath = projectPath, - Type = WorkspaceType.Project, - TargetFrameworks = ["netstandard2.0"], + FilePath = "src", Projects = [ new() { - FilePath = projectPath, + FilePath = Path.GetFileName(projectPath), TargetFrameworks = ["netstandard2.0"], ReferencedProjectPaths = [], ExpectedDependencyCount = 18, @@ -62,7 +58,7 @@ public async Task TestPackageConfig() { var projectPath = "src/project.csproj"; await TestDiscovery( - workspacePath: projectPath, + workspacePath: "", files: new[] { (projectPath, """ @@ -91,9 +87,7 @@ await TestDiscovery( }, expectedResult: new() { - FilePath = projectPath, - Type = WorkspaceType.Project, - TargetFrameworks = ["net45"], + FilePath = "", Projects = [ new() { @@ -119,7 +113,7 @@ public async Task TestProps() { var projectPath = "src/project.csproj"; await TestDiscovery( - workspacePath: projectPath, + workspacePath: "", files: new[] { (projectPath, """ @@ -148,9 +142,7 @@ await TestDiscovery( }, expectedResult: new() { - FilePath = projectPath, - Type = WorkspaceType.Project, - TargetFrameworks = ["netstandard2.0"], + FilePath = "", ExpectedProjectCount = 2, Projects = [ new() @@ -186,7 +178,7 @@ public async Task TestRepo() { var solutionPath = "solution.sln"; await TestDiscovery( - workspacePath: solutionPath, + workspacePath: "", files: new[] { ("src/project.csproj", """ @@ -270,9 +262,7 @@ await TestDiscovery( }, expectedResult: new() { - FilePath = solutionPath, - Type = WorkspaceType.Solution, - TargetFrameworks = ["netstandard2.0"], + FilePath = "", ExpectedProjectCount = 2, Projects = [ new() diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs index dbff5b9d3b5..ccd5c58bae6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs @@ -7,8 +7,6 @@ namespace NuGetUpdater.Core.Test.Discover; public record ExpectedWorkspaceDiscoveryResult : IDiscoveryResult { public required string FilePath { get; init; } - public WorkspaceType Type { get; init; } - public ImmutableArray TargetFrameworks { get; init; } public ImmutableArray Projects { get; init; } public int? ExpectedProjectCount { get; init; } public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/DotNetToolsJsonBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/DotNetToolsJsonBuildFileTests.cs index 30d5339eb64..c975f565ccb 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/DotNetToolsJsonBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/DotNetToolsJsonBuildFileTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Xunit; @@ -30,7 +29,7 @@ public class DotnetToolsJsonBuildFileTests """; private static DotNetToolsJsonBuildFile GetBuildFile() => new( - repoRootPath: "/", + basePath: "/", path: "/.config/dotnet-tools.json", contents: DotnetToolsJson, logger: new Logger(verbose: true)); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs index d71ef9d5336..0d2873fd13b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Xunit; @@ -29,7 +27,7 @@ public class GlobalJsonBuildFileTests """; private static GlobalJsonBuildFile GetBuildFile(string contents) => new( - repoRootPath: "/", + basePath: "/", path: "/global.json", contents: contents, logger: new Logger(verbose: true)); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs index 19b5cea7d81..fbc1fda5173 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.Language.Xml; @@ -28,7 +26,7 @@ public class PackagesConfigBuildFileTests """; private static PackagesConfigBuildFile GetBuildFile(string contents) => new( - repoRootPath: "/", + basePath: "/", path: "/packages.config", contents: Parser.ParseText(contents)); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs index f937725c41c..7803dda83c5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text.RegularExpressions; using Microsoft.Language.Xml; @@ -50,7 +48,7 @@ public class ProjectBuildFileTests """; private static ProjectBuildFile GetBuildFile(string contents, string filename) => new( - repoRootPath: "/", + basePath: "/", path: $"/{filename}", contents: Parser.ParseText(contents)); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs index b21ad5cd43b..0aef2949da5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs @@ -168,7 +168,7 @@ public async Task BuildFileEnumerationWithGlobalJsonWithComments() private static async Task LoadBuildFilesFromTemp(TemporaryDirectory temporaryDirectory, string relativeProjectPath) { var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(temporaryDirectory.DirectoryPath, $"{temporaryDirectory.DirectoryPath}/{relativeProjectPath}"); - var buildFilePaths = buildFiles.Select(f => f.RepoRelativePath.NormalizePathToUnix()).ToArray(); + var buildFilePaths = buildFiles.Select(f => f.RelativePath.NormalizePathToUnix()).ToArray(); return buildFilePaths; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs index 1277ac826d1..6777690b7af 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs @@ -6,20 +6,23 @@ internal static class DirectoryPackagesPropsDiscovery { public static DirectoryPackagesPropsDiscoveryResult? Discover(string repoRootPath, string workspacePath, ImmutableArray projectResults, Logger logger) { - var projectResult = projectResults.FirstOrDefault(p => p.Properties.TryGetValue("ManagePackageVersionsCentrally", out var property) && string.Equals(property.Value, "true", StringComparison.OrdinalIgnoreCase)); + var projectResult = projectResults.FirstOrDefault( + p => p.Properties.TryGetValue("ManagePackageVersionsCentrally", out var property) + && string.Equals(property.Value, "true", StringComparison.OrdinalIgnoreCase)); if (projectResult is null) { + logger.Log(" Central Package Management is not enabled."); return null; } - var projectFilePath = Path.GetFullPath(projectResult.FilePath, repoRootPath); - if (MSBuildHelper.GetDirectoryPackagesPropsPath(repoRootPath, workspacePath) is not { } directoryPackagesPropsPath) + var projectFilePath = Path.GetFullPath(projectResult.FilePath, workspacePath); + if (!MSBuildHelper.TryGetDirectoryPackagesPropsPath(repoRootPath, projectFilePath, out var directoryPackagesPropsPath)) { logger.Log(" No Directory.Packages.props file found."); return null; } - var relativeDirectoryPackagesPropsPath = Path.GetRelativePath(repoRootPath, directoryPackagesPropsPath); + var relativeDirectoryPackagesPropsPath = Path.GetRelativePath(workspacePath, directoryPackagesPropsPath); var directoryPackagesPropsFile = projectResults.FirstOrDefault(p => p.FilePath == relativeDirectoryPackagesPropsPath); if (directoryPackagesPropsFile is null) { @@ -29,7 +32,9 @@ internal static class DirectoryPackagesPropsDiscovery logger.Log($" Discovered [{directoryPackagesPropsFile.FilePath}] file."); - var isTransitivePinningEnabled = projectResult.Properties.TryGetValue("EnableTransitivePinning", out var property) && string.Equals(property.Value, "true", StringComparison.OrdinalIgnoreCase); + var isTransitivePinningEnabled = projectResult.Properties.TryGetValue("EnableTransitivePinning", out var property) + && string.Equals(property.Value, "true", StringComparison.OrdinalIgnoreCase); + return new() { FilePath = directoryPackagesPropsFile.FilePath, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index abfbbf03a79..260ef5ddac3 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -26,61 +26,35 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out { MSBuildHelper.RegisterMSBuild(); - if (!Path.IsPathRooted(workspacePath) || (!File.Exists(workspacePath) && !Directory.Exists(workspacePath))) + // When running under unit tests, the workspace path may not be rooted. + if (!Path.IsPathRooted(workspacePath) || !Directory.Exists(workspacePath)) { workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath)); } - var dotNetToolsJsonDiscovery = DotNetToolsJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); - var globalJsonDiscovery = GlobalJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); + DotNetToolsJsonDiscoveryResult? dotNetToolsJsonDiscovery = null; + GlobalJsonDiscoveryResult? globalJsonDiscovery = null; + DirectoryPackagesPropsDiscoveryResult? directoryPackagesPropsDiscovery = null; - WorkspaceType workspaceType = WorkspaceType.Unknown; ImmutableArray projectResults = []; - if (File.Exists(workspacePath)) + if (Directory.Exists(workspacePath)) { - var extension = Path.GetExtension(workspacePath).ToLowerInvariant(); - switch (extension) - { - case ".sln": - workspaceType = WorkspaceType.Solution; - projectResults = await RunForSolutionAsync(repoRootPath, workspacePath); - break; - case ".proj": - workspaceType = WorkspaceType.DirsProj; - projectResults = await RunForProjFileAsync(repoRootPath, workspacePath); - break; - case ".csproj": - case ".fsproj": - case ".vbproj": - workspaceType = WorkspaceType.Project; - projectResults = await RunForProjectAsync(repoRootPath, workspacePath); - break; - default: - _logger.Log($"File extension [{extension}] is not supported."); - break; - } - } - else if (Directory.Exists(workspacePath)) - { - workspaceType = WorkspaceType.Directory; + dotNetToolsJsonDiscovery = DotNetToolsJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); + globalJsonDiscovery = GlobalJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); + projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath); + + directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger); } else { _logger.Log($"Workspace path [{workspacePath}] does not exist."); } - var directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger); - var result = new WorkspaceDiscoveryResult { - FilePath = Path.GetRelativePath(repoRootPath, workspacePath), - TargetFrameworks = projectResults - .SelectMany(p => p.TargetFrameworks) - .Distinct() - .ToImmutableArray(), - Type = workspaceType, + FilePath = repoRootPath != workspacePath ? Path.GetRelativePath(repoRootPath, workspacePath) : string.Empty, DotNetToolsJson = dotNetToolsJsonDiscovery, GlobalJson = globalJsonDiscovery, DirectoryPackagesProps = directoryPackagesPropsDiscovery, @@ -104,7 +78,7 @@ private async Task> RunForDirectoryAsnyc( return []; } - return await RunForProjectPathsAsync(repoRootPath, projectPaths); + return await RunForProjectPathsAsync(repoRootPath, workspacePath, projectPaths); } private static ImmutableArray FindProjectFiles(string workspacePath) @@ -118,49 +92,10 @@ private static ImmutableArray FindProjectFiles(string workspacePath) .ToImmutableArray(); } - private async Task> RunForSolutionAsync(string repoRootPath, string solutionPath) - { - _logger.Log($"Running for solution [{Path.GetRelativePath(repoRootPath, solutionPath)}]"); - if (!File.Exists(solutionPath)) - { - _logger.Log($"File [{solutionPath}] does not exist."); - return []; - } - - var projectPaths = MSBuildHelper.GetProjectPathsFromSolution(solutionPath); - return await RunForProjectPathsAsync(repoRootPath, projectPaths); - } - - private async Task> RunForProjFileAsync(string repoRootPath, string projFilePath) - { - _logger.Log($"Running for proj file [{Path.GetRelativePath(repoRootPath, projFilePath)}]"); - if (!File.Exists(projFilePath)) - { - _logger.Log($"File [{projFilePath}] does not exist."); - return []; - } - - var projectPaths = MSBuildHelper.GetProjectPathsFromProject(projFilePath); - return await RunForProjectPathsAsync(repoRootPath, projectPaths); - } - - private async Task> RunForProjectAsync(string repoRootPath, string projectFilePath) - { - _logger.Log($"Running for project file [{Path.GetRelativePath(repoRootPath, projectFilePath)}]"); - if (!File.Exists(projectFilePath)) - { - _logger.Log($"File [{projectFilePath}] does not exist."); - return []; - } - - var projectPaths = MSBuildHelper.GetProjectPathsFromProject(projectFilePath).Prepend(projectFilePath); - return await RunForProjectPathsAsync(repoRootPath, projectPaths); - } - - private async Task> RunForProjectPathsAsync(string repoRootPath, IEnumerable projectFilePaths) + private async Task> RunForProjectPathsAsync(string repoRootPath, string workspacePath, IEnumerable projectPaths) { var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var projectPath in projectFilePaths) + foreach (var projectPath in projectPaths) { // If there is some MSBuild logic that needs to run to fully resolve the path skip the project if (!File.Exists(projectPath)) @@ -174,11 +109,11 @@ private async Task> RunForProjectPathsAsy } _processedProjectPaths.Add(projectPath); - var relativeProjectPath = Path.GetRelativePath(repoRootPath, projectPath); - var packagesConfigDependencies = PackagesConfigDiscovery.Discover(repoRootPath, projectPath, _logger) + var relativeProjectPath = Path.GetRelativePath(workspacePath, projectPath); + var packagesConfigDependencies = PackagesConfigDiscovery.Discover(workspacePath, projectPath, _logger) ?.Dependencies; - var projectResults = await SdkProjectDiscovery.DiscoverAsync(repoRootPath, projectPath, _logger); + var projectResults = await SdkProjectDiscovery.DiscoverAsync(repoRootPath, workspacePath, projectPath, _logger); foreach (var projectResult in projectResults) { if (results.ContainsKey(projectResult.FilePath)) @@ -209,6 +144,7 @@ private static async Task WriteResults(string repoRootPath, string outputPath, W var resultPath = Path.IsPathRooted(outputPath) ? outputPath : Path.GetFullPath(outputPath, repoRootPath); + var resultDirectory = Path.GetDirectoryName(resultPath)!; if (!Directory.Exists(resultDirectory)) { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs index dfe72917ee6..72e58bf0f86 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs @@ -6,28 +6,22 @@ internal static class DotNetToolsJsonDiscovery { public static DotNetToolsJsonDiscoveryResult? Discover(string repoRootPath, string workspacePath, Logger logger) { - var dotnetToolsJsonFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); - if (dotnetToolsJsonFile is null) + if (!MSBuildHelper.TryGetDotNetToolsJsonPath(repoRootPath, workspacePath, out var dotnetToolsJsonPath)) { logger.Log(" No dotnet-tools.json file found."); return null; } - logger.Log($" Discovered [{dotnetToolsJsonFile.RepoRelativePath}] file."); + var dotnetToolsJsonFile = DotNetToolsJsonBuildFile.Open(workspacePath, dotnetToolsJsonPath, logger); + + logger.Log($" Discovered [{dotnetToolsJsonFile.RelativePath}] file."); var dependencies = BuildFile.GetDependencies(dotnetToolsJsonFile); return new() { - FilePath = dotnetToolsJsonFile.RepoRelativePath, + FilePath = dotnetToolsJsonFile.RelativePath, Dependencies = dependencies.ToImmutableArray(), }; } - - private static DotNetToolsJsonBuildFile? TryLoadBuildFile(string repoRootPath, string workspacePath, Logger logger) - { - return MSBuildHelper.GetDotNetToolsJsonPath(repoRootPath, workspacePath) is { } dotnetToolsJsonPath - ? DotNetToolsJsonBuildFile.Open(repoRootPath, dotnetToolsJsonPath, logger) - : null; - } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs index 414b9bd7645..b675b57db8e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs @@ -6,28 +6,22 @@ internal static class GlobalJsonDiscovery { public static GlobalJsonDiscoveryResult? Discover(string repoRootPath, string workspacePath, Logger logger) { - var globalJsonFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); - if (globalJsonFile is null) + if (!MSBuildHelper.TryGetGlobalJsonPath(repoRootPath, workspacePath, out var globalJsonPath)) { logger.Log(" No global.json file found."); return null; } - logger.Log($" Discovered [{globalJsonFile.RepoRelativePath}] file."); + var globalJsonFile = GlobalJsonBuildFile.Open(workspacePath, globalJsonPath, logger); + + logger.Log($" Discovered [{globalJsonFile.RelativePath}] file."); var dependencies = BuildFile.GetDependencies(globalJsonFile); return new() { - FilePath = globalJsonFile.RepoRelativePath, + FilePath = globalJsonFile.RelativePath, Dependencies = dependencies.ToImmutableArray(), }; } - - private static GlobalJsonBuildFile? TryLoadBuildFile(string repoRootPath, string workspacePath, Logger logger) - { - return MSBuildHelper.GetGlobalJsonPath(repoRootPath, workspacePath) is { } globalJsonPath - ? GlobalJsonBuildFile.Open(repoRootPath, globalJsonPath, logger) - : null; - } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs index 31871f0e111..4e235d155f6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs @@ -4,30 +4,24 @@ namespace NuGetUpdater.Core.Discover; internal static class PackagesConfigDiscovery { - public static PackagesConfigDiscoveryResult? Discover(string repoRootPath, string workspacePath, Logger logger) + public static PackagesConfigDiscoveryResult? Discover(string workspacePath, string projectPath, Logger logger) { - var packagesConfigFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); - if (packagesConfigFile is null) + if (!NuGetHelper.TryGetPackagesConfigFile(projectPath, out var packagesConfigPath)) { logger.Log(" No packages.config file found."); return null; } - logger.Log($" Discovered [{packagesConfigFile.RepoRelativePath}] file."); + var packagesConfigFile = PackagesConfigBuildFile.Open(workspacePath, packagesConfigPath); + + logger.Log($" Discovered [{packagesConfigFile.RelativePath}] file."); var dependencies = BuildFile.GetDependencies(packagesConfigFile); return new() { - FilePath = packagesConfigFile.RepoRelativePath, + FilePath = packagesConfigFile.RelativePath, Dependencies = dependencies.ToImmutableArray(), }; } - - private static PackagesConfigBuildFile? TryLoadBuildFile(string repoRootPath, string projectPath, Logger logger) - { - return NuGetHelper.TryGetPackagesConfigFile(projectPath, out var packagesConfigPath) - ? PackagesConfigBuildFile.Open(repoRootPath, packagesConfigPath) - : null; - } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs index c3966ac7c9b..e769297eb9a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs @@ -4,7 +4,7 @@ namespace NuGetUpdater.Core.Discover; internal static class SdkProjectDiscovery { - public static async Task> DiscoverAsync(string repoRootPath, string projectPath, Logger logger) + public static async Task> DiscoverAsync(string repoRootPath, string workspacePath, string projectPath, Logger logger) { // Determine which targets and props files contribute to the build. var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath); @@ -45,7 +45,7 @@ public static async Task> DiscoverAsync(s results.Add(new() { - FilePath = buildFile.RepoRelativePath, + FilePath = Path.GetRelativePath(workspacePath, buildFile.Path), Properties = properties, TargetFrameworks = tfms, ReferencedProjectPaths = referencedProjectPaths, @@ -56,7 +56,7 @@ public static async Task> DiscoverAsync(s { results.Add(new() { - FilePath = buildFile.RepoRelativePath, + FilePath = Path.GetRelativePath(workspacePath, buildFile.Path), Properties = ImmutableDictionary.Empty, Dependencies = directDependencies, }); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs index 743ef136f93..52c36627227 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs @@ -5,19 +5,8 @@ namespace NuGetUpdater.Core.Discover; public sealed record WorkspaceDiscoveryResult : IDiscoveryResult { public required string FilePath { get; init; } - public WorkspaceType Type { get; init; } - public ImmutableArray TargetFrameworks { get; init; } public ImmutableArray Projects { get; init; } public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } public GlobalJsonDiscoveryResult? GlobalJson { get; init; } public DotNetToolsJsonDiscoveryResult? DotNetToolsJson { get; init; } } - -public enum WorkspaceType -{ - Unknown, - Directory, - Solution, - DirsProj, - Project, -} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs index e0160900e3c..82572673c0b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text.RegularExpressions; -using System.Threading.Tasks; using DiffPlex; using DiffPlex.DiffBuilder; @@ -12,13 +8,13 @@ namespace NuGetUpdater.Core; internal abstract class BuildFile { - public string RepoRootPath { get; } + public string BasePath { get; } public string Path { get; } - public string RepoRelativePath => System.IO.Path.GetRelativePath(RepoRootPath, Path); + public string RelativePath => System.IO.Path.GetRelativePath(BasePath, Path); - public BuildFile(string repoRootPath, string path) + public BuildFile(string basePath, string path) { - RepoRootPath = repoRootPath; + BasePath = basePath; Path = path; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/DotNetToolsJsonBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/DotNetToolsJsonBuildFile.cs index 4031536742e..e02ca26147b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/DotNetToolsJsonBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/DotNetToolsJsonBuildFile.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text.Json.Nodes; namespace NuGetUpdater.Core; internal sealed class DotNetToolsJsonBuildFile : JsonBuildFile { - public static DotNetToolsJsonBuildFile Open(string repoRootPath, string path, Logger logger) - => new(repoRootPath, path, File.ReadAllText(path), logger); + public static DotNetToolsJsonBuildFile Open(string basePath, string path, Logger logger) + => new(basePath, path, File.ReadAllText(path), logger); - public DotNetToolsJsonBuildFile(string repoRootPath, string path, string contents, Logger logger) - : base(repoRootPath, path, contents, logger) + public DotNetToolsJsonBuildFile(string basePath, string path, string contents, Logger logger) + : base(basePath, path, contents, logger) { } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs index e86372c991d..b21595787d7 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs @@ -1,35 +1,20 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text.Json.Nodes; namespace NuGetUpdater.Core; internal sealed class GlobalJsonBuildFile : JsonBuildFile { - public static GlobalJsonBuildFile Open(string repoRootPath, string path, Logger logger) - => new(repoRootPath, path, File.ReadAllText(path), logger); + public static GlobalJsonBuildFile Open(string basePath, string path, Logger logger) + => new(basePath, path, File.ReadAllText(path), logger); - public GlobalJsonBuildFile(string repoRootPath, string path, string contents, Logger logger) - : base(repoRootPath, path, contents, logger) + public GlobalJsonBuildFile(string basePath, string path, string contents, Logger logger) + : base(basePath, path, contents, logger) { } - public JsonObject? Sdk - { - get - { - return Node.Value is JsonObject root ? root["sdk"]?.AsObject() : null; - } - } + public JsonObject? Sdk => Node.Value is JsonObject root ? root["sdk"]?.AsObject() : null; - public JsonObject? MSBuildSdks - { - get - { - return Node.Value is JsonObject root ? root["msbuild-sdks"]?.AsObject() : null; - } - } + public JsonObject? MSBuildSdks => Node.Value is JsonObject root ? root["msbuild-sdks"]?.AsObject() : null; public IEnumerable GetDependencies() => MSBuildSdks?.AsObject().Select( t => new Dependency(t.Key, t.Value?.GetValue() ?? string.Empty, DependencyType.MSBuildSdk)) ?? Enumerable.Empty(); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs index 009692c7bf0..d7790d79910 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs @@ -1,4 +1,3 @@ -using System; using System.Text.Json; using System.Text.Json.Nodes; @@ -40,7 +39,7 @@ private void ResetNode() { // We can't police that people have legal JSON files. // If they don't, we just return null. - logger.Log($"Failed to parse JSON file: {RepoRelativePath}, got {ex}"); + logger.Log($"Failed to parse JSON file: {RelativePath}, got {ex}"); return null; } }); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs index 1c7b26222ea..25c15252df9 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs @@ -1,22 +1,17 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core; internal sealed class PackagesConfigBuildFile : XmlBuildFile { - public static PackagesConfigBuildFile Open(string repoRootPath, string path) - => Parse(repoRootPath, path, File.ReadAllText(path)); + public static PackagesConfigBuildFile Open(string basePath, string path) + => Parse(basePath, path, File.ReadAllText(path)); - public static PackagesConfigBuildFile Parse(string repoRootPath, string path, string xml) - => new(repoRootPath, path, Parser.ParseText(xml)); + public static PackagesConfigBuildFile Parse(string basePath, string path, string xml) + => new(basePath, path, Parser.ParseText(xml)); - public PackagesConfigBuildFile(string repoRootPath, string path, XmlDocumentSyntax contents) - : base(repoRootPath, path, contents) + public PackagesConfigBuildFile(string basePath, string path, XmlDocumentSyntax contents) + : base(basePath, path, contents) { } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs index 99f24254aa8..1bf59650084 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs @@ -4,14 +4,14 @@ namespace NuGetUpdater.Core; internal sealed class ProjectBuildFile : XmlBuildFile { - public static ProjectBuildFile Open(string repoRootPath, string path) - => Parse(repoRootPath, path, File.ReadAllText(path)); + public static ProjectBuildFile Open(string basePath, string path) + => Parse(basePath, path, File.ReadAllText(path)); - public static ProjectBuildFile Parse(string repoRootPath, string path, string xml) - => new(repoRootPath, path, Parser.ParseText(xml)); + public static ProjectBuildFile Parse(string basePath, string path, string xml) + => new(basePath, path, Parser.ParseText(xml)); - public ProjectBuildFile(string repoRootPath, string path, XmlDocumentSyntax contents) - : base(repoRootPath, path, contents) + public ProjectBuildFile(string basePath, string path, XmlDocumentSyntax contents) + : base(basePath, path, contents) { } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/XmlBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/XmlBuildFile.cs index 36c2988efab..2af2a36fcba 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/XmlBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/XmlBuildFile.cs @@ -4,8 +4,8 @@ namespace NuGetUpdater.Core; internal abstract class XmlBuildFile : BuildFile { - public XmlBuildFile(string repoRootPath, string path, XmlDocumentSyntax contents) - : base(repoRootPath, path, contents) + public XmlBuildFile(string basePath, string path, XmlDocumentSyntax contents) + : base(basePath, path, contents) { } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs index cd1a9ed31b6..36f529938df 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs @@ -2,17 +2,23 @@ namespace NuGetUpdater.Core; internal static class DotNetToolsJsonUpdater { - public static async Task UpdateDependencyAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, + public static async Task UpdateDependencyAsync( + string repoRootPath, + string workspacePath, + string dependencyName, + string previousDependencyVersion, + string newDependencyVersion, Logger logger) { - var dotnetToolsJsonFile = TryLoadBuildFile(repoRootPath, workspacePath, logger); - if (dotnetToolsJsonFile is null) + if (!MSBuildHelper.TryGetDotNetToolsJsonPath(repoRootPath, workspacePath, out var dotnetToolsJsonPath)) { logger.Log(" No dotnet-tools.json file found."); return; } - logger.Log($" Updating [{dotnetToolsJsonFile.RepoRelativePath}] file."); + var dotnetToolsJsonFile = DotNetToolsJsonBuildFile.Open(repoRootPath, dotnetToolsJsonPath, logger); + + logger.Log($" Updating [{dotnetToolsJsonFile.RelativePath}] file."); var containsDependency = dotnetToolsJsonFile.GetDependencies().Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)); if (!containsDependency) @@ -33,15 +39,8 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string works if (await dotnetToolsJsonFile.SaveAsync()) { - logger.Log($" Saved [{dotnetToolsJsonFile.RepoRelativePath}]."); + logger.Log($" Saved [{dotnetToolsJsonFile.RelativePath}]."); } } } - - private static DotNetToolsJsonBuildFile? TryLoadBuildFile(string repoRootPath, string workspacePath, Logger logger) - { - return MSBuildHelper.GetDotNetToolsJsonPath(repoRootPath, workspacePath) is { } dotnetToolsJsonPath - ? DotNetToolsJsonBuildFile.Open(repoRootPath, dotnetToolsJsonPath, logger) - : null; - } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs index 7488c9f061d..596288703ee 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs @@ -1,7 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - namespace NuGetUpdater.Core; internal static class GlobalJsonUpdater @@ -14,14 +10,15 @@ public static async Task UpdateDependencyAsync( string newDependencyVersion, Logger logger) { - var globalJsonFile = LoadBuildFile(repoRootPath, workspacePath, logger); - if (globalJsonFile is null) + if (!MSBuildHelper.TryGetGlobalJsonPath(repoRootPath, workspacePath, out var globalJsonPath)) { logger.Log(" No global.json file found."); return; } - logger.Log($" Updating [{globalJsonFile.RepoRelativePath}] file."); + var globalJsonFile = GlobalJsonBuildFile.Open(repoRootPath, globalJsonPath, logger); + + logger.Log($" Updating [{globalJsonFile.RelativePath}] file."); var containsDependency = globalJsonFile.GetDependencies().Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)); if (!containsDependency) @@ -46,14 +43,7 @@ public static async Task UpdateDependencyAsync( if (await globalJsonFile.SaveAsync()) { - logger.Log($" Saved [{globalJsonFile.RepoRelativePath}]."); + logger.Log($" Saved [{globalJsonFile.RelativePath}]."); } } - - private static GlobalJsonBuildFile? LoadBuildFile(string repoRootPath, string workspacePath, Logger logger) - { - return MSBuildHelper.GetGlobalJsonPath(repoRootPath, workspacePath) is { } globalJsonPath - ? GlobalJsonBuildFile.Open(repoRootPath, globalJsonPath, logger) - : null; - } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 3d75b555bb1..9f4b22d1c80 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -356,12 +356,12 @@ private static UpdateResult TryUpdateDependencyVersion( var currentVersion = versionAttribute.Value.TrimStart('[', '(').TrimEnd(']', ')'); if (currentVersion.Contains(',') || currentVersion.Contains('*')) { - logger.Log($" Found unsupported [{packageNode.Name}] version attribute value [{versionAttribute.Value}] in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found unsupported [{packageNode.Name}] version attribute value [{versionAttribute.Value}] in [{buildFile.RelativePath}]."); foundUnsupported = true; } else if (string.Equals(currentVersion, previousDependencyVersion, StringComparison.Ordinal)) { - logger.Log($" Found incorrect [{packageNode.Name}] version attribute in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found incorrect [{packageNode.Name}] version attribute in [{buildFile.RelativePath}]."); updateNodes.Add(versionAttribute); } else if (previousDependencyVersion == null && NuGetVersion.TryParse(currentVersion, out var previousVersion)) @@ -371,13 +371,13 @@ private static UpdateResult TryUpdateDependencyVersion( { previousPackageVersion = currentVersion; - logger.Log($" Found incorrect peer [{packageNode.Name}] version attribute in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found incorrect peer [{packageNode.Name}] version attribute in [{buildFile.RelativePath}]."); updateNodes.Add(versionAttribute); } } else if (string.Equals(currentVersion, newDependencyVersion, StringComparison.Ordinal)) { - logger.Log($" Found correct [{packageNode.Name}] version attribute in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found correct [{packageNode.Name}] version attribute in [{buildFile.RelativePath}]."); foundCorrect = true; } } @@ -394,12 +394,12 @@ private static UpdateResult TryUpdateDependencyVersion( var currentVersion = versionValue.TrimStart('[', '(').TrimEnd(']', ')'); if (currentVersion.Contains(',') || currentVersion.Contains('*')) { - logger.Log($" Found unsupported [{packageNode.Name}] version node value [{versionValue}] in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found unsupported [{packageNode.Name}] version node value [{versionValue}] in [{buildFile.RelativePath}]."); foundUnsupported = true; } else if (currentVersion == previousDependencyVersion) { - logger.Log($" Found incorrect [{packageNode.Name}] version node in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found incorrect [{packageNode.Name}] version node in [{buildFile.RelativePath}]."); if (versionElement is XmlElementSyntax elementSyntax) { updateNodes.Add(elementSyntax); @@ -416,7 +416,7 @@ private static UpdateResult TryUpdateDependencyVersion( { previousPackageVersion = currentVersion; - logger.Log($" Found incorrect peer [{packageNode.Name}] version node in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found incorrect peer [{packageNode.Name}] version node in [{buildFile.RelativePath}]."); if (versionElement is XmlElementSyntax elementSyntax) { updateNodes.Add(elementSyntax); @@ -430,7 +430,7 @@ private static UpdateResult TryUpdateDependencyVersion( } else if (currentVersion == newDependencyVersion) { - logger.Log($" Found correct [{packageNode.Name}] version node in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found correct [{packageNode.Name}] version node in [{buildFile.RelativePath}]."); foundCorrect = true; } } @@ -506,12 +506,12 @@ private static UpdateResult TryUpdateDependencyVersion( var currentVersion = propertyContents.TrimStart('[', '(').TrimEnd(']', ')'); if (currentVersion.Contains(',') || currentVersion.Contains('*')) { - logger.Log($" Found unsupported version property [{propertyElement.Name}] value [{propertyContents}] in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found unsupported version property [{propertyElement.Name}] value [{propertyContents}] in [{buildFile.RelativePath}]."); foundUnsupported = true; } else if (currentVersion == previousDependencyVersion) { - logger.Log($" Found incorrect version property [{propertyElement.Name}] in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found incorrect version property [{propertyElement.Name}] in [{buildFile.RelativePath}]."); updateProperties.Add((XmlElementSyntax)propertyElement.AsNode); } else if (previousDependencyVersion is null && NuGetVersion.TryParse(currentVersion, out var previousVersion)) @@ -521,13 +521,13 @@ private static UpdateResult TryUpdateDependencyVersion( { previousPackageVersion = currentVersion; - logger.Log($" Found incorrect peer version property [{propertyElement.Name}] in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found incorrect peer version property [{propertyElement.Name}] in [{buildFile.RelativePath}]."); updateProperties.Add((XmlElementSyntax)propertyElement.AsNode); } } else if (currentVersion == newDependencyVersion) { - logger.Log($" Found correct version property [{propertyElement.Name}] in [{buildFile.RepoRelativePath}]."); + logger.Log($" Found correct version property [{propertyElement.Name}] in [{buildFile.RelativePath}]."); foundCorrect = true; } } @@ -585,7 +585,7 @@ private static async Task SaveBuildFilesAsync(ImmutableArray b { if (await buildFile.SaveAsync()) { - logger.Log($" Saved [{buildFile.RepoRelativePath}]."); + logger.Log($" Saved [{buildFile.RelativePath}]."); } } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 40173460eea..6d0c2105d49 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -66,7 +66,7 @@ public static string[] GetTargetFrameworkMonikers(ImmutableArray GetProperties(ImmutableArray string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase); if (hasEmptyCondition || conditionIsCheckingForEmptyString) { - properties[property.Name] = new(property.Name, property.Value, buildFile.RepoRelativePath); + properties[property.Name] = new(property.Name, property.Value, buildFile.RelativePath); } } } @@ -246,7 +246,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase); if (hasEmptyCondition || conditionIsCheckingForEmptyString) { - propertyInfo[property.Name] = new(property.Name, property.Value, buildFile.RepoRelativePath); + propertyInfo[property.Name] = new(property.Name, property.Value, buildFile.RelativePath); } } } @@ -518,19 +518,22 @@ internal static async Task GetAllPackageDependenciesAsync( } } - internal static string? GetGlobalJsonPath(string repoRootPath, string workspacePath) + internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath) { - return PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json"); + globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json"); + return globalJsonPath is not null; } - internal static string? GetDotNetToolsJsonPath(string repoRootPath, string workspacePath) + internal static bool TryGetDotNetToolsJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? dotnetToolsJsonJsonPath) { - return PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./.config/dotnet-tools.json"); + dotnetToolsJsonJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./.config/dotnet-tools.json"); + return dotnetToolsJsonJsonPath is not null; } - internal static string? GetDirectoryPackagesPropsPath(string repoRootPath, string workspacePath) + internal static bool TryGetDirectoryPackagesPropsPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? directoryPackagesPropsPath) { - return PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./Directory.Packages.props"); + directoryPackagesPropsPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./Directory.Packages.props"); + return directoryPackagesPropsPath is not null; } internal static async Task> LoadBuildFilesAsync(string repoRootPath, string projectPath) @@ -541,7 +544,7 @@ internal static async Task> LoadBuildFilesAsync }; // a global.json file might cause problems with the dotnet msbuild command; create a safe version temporarily - var globalJsonPath = GetGlobalJsonPath(repoRootPath, projectPath); + TryGetGlobalJsonPath(repoRootPath, projectPath, out var globalJsonPath); var safeGlobalJsonName = $"{globalJsonPath}{Guid.NewGuid()}"; try diff --git a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb index 8c0086f65d1..29827953c74 100644 --- a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb @@ -14,8 +14,6 @@ class WorkspaceDiscovery sig { params(json: T::Hash[String, T.untyped]).returns(WorkspaceDiscovery) } def self.from_json(json) file_path = T.let(json.fetch("FilePath"), String) - type = T.let(json.fetch("Type"), String) - target_frameworks = T.let(json.fetch("TargetFrameworks"), T::Array[String]) projects = T.let(json.fetch("Projects"), T::Array[T::Hash[String, T.untyped]]).filter_map do |project| ProjectDiscovery.from_json(project) end @@ -28,8 +26,6 @@ def self.from_json(json) .from_json(T.let(json.fetch("DotNetToolsJson"), T.nilable(T::Hash[String, T.untyped]))) WorkspaceDiscovery.new(file_path: file_path, - type: type, - target_frameworks: target_frameworks, projects: projects, directory_packages_props: directory_packages_props, global_json: global_json, @@ -38,18 +34,13 @@ def self.from_json(json) sig do params(file_path: String, - type: String, - target_frameworks: T::Array[String], projects: T::Array[ProjectDiscovery], directory_packages_props: T.nilable(DirectoryPackagesPropsDiscovery), global_json: T.nilable(DependencyFileDiscovery), dotnet_tools_json: T.nilable(DependencyFileDiscovery)).void end - def initialize(file_path:, type:, target_frameworks:, projects:, directory_packages_props:, global_json:, - dotnet_tools_json:) + def initialize(file_path:, projects:, directory_packages_props:, global_json:, dotnet_tools_json:) @file_path = file_path - @type = type - @target_frameworks = target_frameworks @projects = projects @directory_packages_props = directory_packages_props @global_json = global_json @@ -59,12 +50,6 @@ def initialize(file_path:, type:, target_frameworks:, projects:, directory_packa sig { returns(String) } attr_reader :file_path - sig { returns(String) } - attr_reader :type - - sig { returns(T::Array[String]) } - attr_reader :target_frameworks - sig { returns(T::Array[ProjectDiscovery]) } attr_reader :projects From c015e395c6e2e9b911d06d6cfb1787f3740de253 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Thu, 14 Mar 2024 23:11:00 -0700 Subject: [PATCH 13/26] Include transitive packages from all TFMs --- .../EntryPointTests.Discover.cs | 6 +-- .../Discover/DiscoveryWorkerTestBase.cs | 1 + .../Discover/DiscoveryWorkerTests.cs | 14 ++--- .../Utilities/AssertEx.cs | 51 ++++++++++++++----- .../NuGetUpdater.Core/Dependency.cs | 3 ++ .../Discover/DiscoveryWorker.cs | 4 ++ .../Discover/SdkProjectDiscovery.cs | 39 +++++++++++++- .../Updater/SdkPackageUpdater.cs | 4 +- .../Utilities/MSBuildHelper.cs | 3 +- 9 files changed, 96 insertions(+), 29 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index 34d5df6d762..09fdea7fada 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -86,7 +86,7 @@ await RunAsync(path => ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig, TargetFrameworks: ["net45"]), ], Properties = new Dictionary() { @@ -146,7 +146,7 @@ await RunAsync(path => ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig, TargetFrameworks: ["net45"]) ], Properties = new Dictionary() { @@ -206,7 +206,7 @@ await RunAsync(path => ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig, TargetFrameworks: ["net45"]) ], Properties = new Dictionary() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index d0ad18c8750..ab194782b23 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -83,6 +83,7 @@ void ValidateDependencies(ImmutableArray expectedDependencies, Immut Assert.Equal(expectedDependency.Name, actualDependency.Name); Assert.Equal(expectedDependency.Version, actualDependency.Version); Assert.Equal(expectedDependency.Type, actualDependency.Type); + AssertEx.Equal(expectedDependency.TargetFrameworks, actualDependency.TargetFrameworks); Assert.Equal(expectedDependency.IsDirect, actualDependency.IsDirect); Assert.Equal(expectedDependency.IsTransitive, actualDependency.IsTransitive); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs index 7ee55499e87..08544863cf2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -40,7 +40,7 @@ await TestDiscovery( ReferencedProjectPaths = [], ExpectedDependencyCount = 18, Dependencies = [ - new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, TargetFrameworks: ["netstandard2.0"], IsDirect: true) ], Properties = new Dictionary() { @@ -96,7 +96,7 @@ await TestDiscovery( ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig) + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig, TargetFrameworks: ["net45"]) ], Properties = new Dictionary() { @@ -152,7 +152,7 @@ await TestDiscovery( ReferencedProjectPaths = [], ExpectedDependencyCount = 18, Dependencies = [ - new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, TargetFrameworks: ["netstandard2.0"], IsDirect: true) ], Properties = new Dictionary() { @@ -184,7 +184,7 @@ await TestDiscovery( ("src/project.csproj", """ - netstandard2.0 + netstandard2.0;net6.0 @@ -268,17 +268,17 @@ await TestDiscovery( new() { FilePath = "src/project.csproj", - TargetFrameworks = ["netstandard2.0"], + TargetFrameworks = ["netstandard2.0", "net6.0"], ReferencedProjectPaths = [], ExpectedDependencyCount = 18, Dependencies = [ - new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, IsDirect: true) + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, TargetFrameworks: ["netstandard2.0", "net6.0"], IsDirect: true) ], Properties = new Dictionary() { ["ManagePackageVersionsCentrally"] = new("ManagePackageVersionsCentrally", "true", "Directory.Packages.props"), ["NewtonsoftJsonPackageVersion"] = new("NewtonsoftJsonPackageVersion", "9.0.1", "Directory.Packages.props"), - ["TargetFramework"] = new("TargetFramework", "netstandard2.0", "src/project.csproj"), + ["TargetFrameworks"] = new("TargetFrameworks", "netstandard2.0;net6.0", "src/project.csproj"), }.ToImmutableDictionary() } ], diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs index 84f093fd318..f3fe6edec43 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs @@ -12,40 +12,63 @@ namespace NuGetUpdater.Core.Test.Utilities; /// public static class AssertEx { + public static void Equal( + ImmutableArray? expected, + ImmutableArray? actual, + IEqualityComparer? comparer = null, + string? message = null) + { + if (actual is null || actual.Value.IsDefault) + { + Assert.True(expected is null || expected.Value.IsDefault, message); + } + else + { + Equal(expected, (IEnumerable)actual.Value, comparer, message); + } + } + public static void Equal( ImmutableArray expected, ImmutableArray actual, IEqualityComparer? comparer = null, string? message = null) { - Equal(expected, (IEnumerable)actual, comparer, message); + if (actual.IsDefault) + { + Assert.True(expected.IsDefault, message); + } + else + { + Equal(expected, (IEnumerable)actual, comparer, message); + } } public static void Equal( - ImmutableArray expected, - IEnumerable actual, + ImmutableArray? expected, + IEnumerable? actual, IEqualityComparer? comparer = null, string? message = null) { - if (actual == null || expected.IsDefault) + if (expected is null || expected.Value.IsDefault) { - Assert.True((actual == null) == expected.IsDefault, message); + Assert.True(actual is null, message); } else { - Equal((IEnumerable)expected, actual, comparer, message); + Equal((IEnumerable?)expected, actual, comparer, message); } } public static void Equal( - IEnumerable expected, - ImmutableArray actual, + IEnumerable? expected, + ImmutableArray? actual, IEqualityComparer? comparer = null, string? message = null) { - if (expected == null || actual.IsDefault) + if (actual is null || actual.Value.IsDefault) { - Assert.True((expected == null) == actual.IsDefault, message); + Assert.True(expected is null, message); } else { @@ -54,19 +77,19 @@ public static void Equal( } public static void Equal( - IEnumerable expected, - IEnumerable actual, + IEnumerable? expected, + IEnumerable? actual, IEqualityComparer? comparer = null, string? message = null) { if (expected == null) { - Assert.Null(actual); + Assert.True(actual is null, message); return; } else { - Assert.NotNull(actual); + Assert.True(actual is not null, message); } if (SequenceEqual(expected, actual, comparer)) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs index 0c8f55f957d..4f4e0645904 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; + namespace NuGetUpdater.Core; public sealed record Dependency( @@ -5,6 +7,7 @@ public sealed record Dependency( string? Version, DependencyType Type, EvaluationResult? EvaluationResult = null, + ImmutableArray? TargetFrameworks = null, bool IsDevDependency = false, bool IsDirect = false, bool IsTransitive = false, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index 260ef5ddac3..3b7191cb5b5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -124,6 +124,10 @@ private async Task> RunForProjectPathsAsy // If we had packages.config dependencies, merge them with the project dependencies if (projectResult.FilePath == relativeProjectPath && packagesConfigDependencies is not null) { + packagesConfigDependencies = packagesConfigDependencies.Value + .Select(d => d with { TargetFrameworks = projectResult.TargetFrameworks }) + .ToImmutableArray(); + results[projectResult.FilePath] = projectResult with { Dependencies = [.. projectResult.Dependencies, .. packagesConfigDependencies], diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs index e769297eb9a..12988d3f4c7 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs @@ -1,5 +1,7 @@ using System.Collections.Immutable; +using NuGet.Versioning; + namespace NuGetUpdater.Core.Discover; internal static class SdkProjectDiscovery @@ -40,8 +42,11 @@ public static async Task> DiscoverAsync(s var referencedProjectPaths = MSBuildHelper.GetProjectPathsFromProject(projectPath).ToImmutableArray(); // Get the complete set of dependencies including transitive dependencies. - var allDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(repoRootPath, projectPath, tfms.First(), directDependencies, logger); - var dependencies = directDependencies.Concat(allDependencies.Where(d => d.IsTransitive)).ToImmutableArray(); + directDependencies = directDependencies + .Select(d => d with { TargetFrameworks = tfms }) + .ToImmutableArray(); + var transitiveDependencies = await GetTransitiveDependencies(repoRootPath, projectPath, tfms, directDependencies, logger); + ImmutableArray dependencies = [.. directDependencies, .. transitiveDependencies]; results.Add(new() { @@ -65,4 +70,34 @@ public static async Task> DiscoverAsync(s return results.ToImmutable(); } + + private static async Task> GetTransitiveDependencies(string repoRootPath, string projectPath, ImmutableArray tfms, ImmutableArray directDependencies, Logger logger) + { + Dictionary transitiveDependencies = new(StringComparer.OrdinalIgnoreCase); + foreach (var tfm in tfms) + { + var tfmDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(repoRootPath, projectPath, tfm, directDependencies, logger); + foreach (var dependency in tfmDependencies.Where(d => d.IsTransitive)) + { + if (!transitiveDependencies.TryGetValue(dependency.Name, out var existingDependency)) + { + transitiveDependencies[dependency.Name] = dependency; + continue; + } + + transitiveDependencies[dependency.Name] = existingDependency with + { + // Revisit this logic. We may want to return each dependency instead of merging them. + Version = SemanticVersion.Parse(existingDependency.Version!) > SemanticVersion.Parse(dependency.Version!) + ? existingDependency.Version + : dependency.Version, + TargetFrameworks = existingDependency.TargetFrameworks is not null && dependency.TargetFrameworks is not null + ? existingDependency.TargetFrameworks.Value.AddRange(dependency.TargetFrameworks) + : existingDependency.TargetFrameworks ?? dependency.TargetFrameworks, + }; + } + } + + return [.. transitiveDependencies.Values]; + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 9f4b22d1c80..8ad45c2d70a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -84,7 +84,7 @@ private static async Task DoesDependencyRequireUpdateAsync( tfm, topLevelDependencies, logger); - foreach (var (packageName, packageVersion, _, _, _, _, _, _, _) in dependencies) + foreach (var (packageName, packageVersion, _, _, _, _, _, _, _, _) in dependencies) { if (packageVersion is null) { @@ -269,7 +269,7 @@ private static async Task AddTransitiveDependencyAsync(string projectPath, strin var packagesAndVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var (_, dependencies) in tfmsAndDependencies) { - foreach (var (packageName, packageVersion, _, _, _, _, _, _, _) in dependencies) + foreach (var (packageName, packageVersion, _, _, _, _, _, _, _, _) in dependencies) { if (packagesAndVersions.TryGetValue(packageName, out var existingVersion) && existingVersion != packageVersion) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 6d0c2105d49..1b5a4684ebe 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -485,6 +485,7 @@ internal static async Task GetAllPackageDependenciesAsync( if (exitCode == 0) { + ImmutableArray tfms = [targetFramework]; var lines = stdout.Split('\n').Select(line => line.Trim()); var pattern = PackagePattern(); var allDependencies = lines @@ -494,7 +495,7 @@ internal static async Task GetAllPackageDependenciesAsync( { var packageName = match.Groups["PackageName"].Value; var isTransitive = !topLevelPackagesNames.Contains(packageName); - return new Dependency(packageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, IsTransitive: isTransitive); + return new Dependency(packageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, TargetFrameworks: tfms, IsTransitive: isTransitive); }) .ToArray(); From 010c4c1df1d2bdb1e9829be822423c49a4be1e51 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 18 Mar 2024 13:57:33 -0700 Subject: [PATCH 14/26] Fixup remaining .NET tests --- .../Utilities/MSBuildHelperTests.cs | 240 +++++++++--------- .../NuGetUpdater.Core/Dependency.cs | 45 +++- .../Discover/DiscoveryWorker.cs | 11 +- .../Discover/SdkProjectDiscovery.cs | 11 +- .../Updater/SdkPackageUpdater.cs | 6 +- .../Utilities/ImmutableArrayExtensions.cs | 18 ++ .../Utilities/MSBuildHelper.cs | 21 +- 7 files changed, 215 insertions(+), 137 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ImmutableArrayExtensions.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs index c23fb922b30..736770fa937 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -118,7 +118,7 @@ public void TfmsCanBeDeterminedFromProjectContents(string projectContents, strin File.WriteAllText(projectPath, projectContents); var expectedTfms = new[] { expectedTfm1, expectedTfm2 }.Where(tfm => tfm is not null).ToArray(); var buildFile = ProjectBuildFile.Open(Path.GetDirectoryName(projectPath)!, projectPath); - var actualTfms = MSBuildHelper.GetTargetFrameworkMonikers(ImmutableArray.Create(buildFile)); + var actualTfms = MSBuildHelper.GetTargetFrameworkMonikers(ImmutableArray.Create(buildFile), []); AssertEx.Equal(expectedTfms, actualTfms); } finally @@ -140,7 +140,7 @@ public async Task TopLevelPackageDependenciesCanBeDetermined(TestFile[] buildFil buildFiles.Add(ProjectBuildFile.Parse(testDirectory.DirectoryPath, fullPath, content)); } - var actualTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles.ToImmutableArray()); + var actualTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles.ToImmutableArray(), []); AssertEx.Equal(expectedTopLevelDependencies, actualTopLevelDependencies); } @@ -150,22 +150,22 @@ public async Task AllPackageDependenciesCanBeTraversed() using var temp = new TemporaryDirectory(); var expectedDependencies = new Dependency[] { - new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Buffers", "4.5.1", DependencyType.Unknown, IsTransitive: true), - new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Memory", "4.5.5", DependencyType.Unknown, IsTransitive: true), - new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, IsTransitive: true), - new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, IsTransitive: true), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Buffers", "4.5.1", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Memory", "4.5.5", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), }; var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( temp.DirectoryPath, @@ -181,79 +181,79 @@ public async Task AllPackageDependencies_DoNotTruncateLongDependencyLists() using var temp = new TemporaryDirectory(); var expectedDependencies = new Dependency[] { - new("Castle.Core", "4.4.1", DependencyType.Unknown), - new("Microsoft.ApplicationInsights", "2.10.0", DependencyType.Unknown), - new("Microsoft.ApplicationInsights.Agent.Intercept", "2.4.0", DependencyType.Unknown), - new("Microsoft.ApplicationInsights.DependencyCollector", "2.10.0", DependencyType.Unknown), - new("Microsoft.ApplicationInsights.PerfCounterCollector", "2.10.0", DependencyType.Unknown), - new("Microsoft.ApplicationInsights.WindowsServer", "2.10.0", DependencyType.Unknown), - new("Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel", "2.10.0", DependencyType.Unknown), - new("Microsoft.AspNet.TelemetryCorrelation", "1.0.5", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Caching.Abstractions", "1.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Caching.Memory", "1.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.DiagnosticAdapter", "1.1.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.PlatformAbstractions", "1.1.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Moq", "4.16.1", DependencyType.Unknown), - new("MSTest.TestFramework", "2.1.0", DependencyType.Unknown), - new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown), - new("System", "4.1.311.2", DependencyType.Unknown), - new("System.Buffers", "4.5.1", DependencyType.Unknown, IsTransitive: true), - new("System.Collections.Concurrent", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Collections.Immutable", "1.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Collections.NonGeneric", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Collections.Specialized", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.ComponentModel", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.ComponentModel.Primitives", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.ComponentModel.TypeConverter", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Core", "3.5.21022.801", DependencyType.Unknown), - new("System.Data.Common", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Diagnostics.PerformanceCounter", "4.5.0", DependencyType.Unknown, IsTransitive: true), - new("System.Diagnostics.StackTrace", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Dynamic.Runtime", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.IO.FileSystem.Primitives", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Linq", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Linq.Expressions", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Memory", "4.5.5", DependencyType.Unknown, IsTransitive: true), - new("System.Net.WebHeaderCollection", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, IsTransitive: true), - new("System.ObjectModel", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Private.DataContractSerialization", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Reflection.Emit", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Reflection.Emit.ILGeneration", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Reflection.Emit.Lightweight", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Reflection.Metadata", "1.4.1", DependencyType.Unknown, IsTransitive: true), - new("System.Reflection.TypeExtensions", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Runtime.InteropServices.RuntimeInformation", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Runtime.Numerics", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Runtime.Serialization.Json", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Runtime.Serialization.Primitives", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Security.Claims", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Security.Cryptography.OpenSsl", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Security.Cryptography.Primitives", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Security.Principal", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Text.RegularExpressions", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Threading", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, IsTransitive: true), - new("System.Threading.Thread", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Threading.ThreadPool", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Xml.ReaderWriter", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Xml.XDocument", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Xml.XmlDocument", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("System.Xml.XmlSerializer", "4.3.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.ApplicationInsights.Web", "2.10.0", DependencyType.Unknown), - new("MSTest.TestAdapter", "2.1.0", DependencyType.Unknown), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), + new("Castle.Core", "4.4.1", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.ApplicationInsights", "2.10.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.ApplicationInsights.Agent.Intercept", "2.4.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.ApplicationInsights.DependencyCollector", "2.10.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.ApplicationInsights.PerfCounterCollector", "2.10.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.ApplicationInsights.WindowsServer", "2.10.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel", "2.10.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.AspNet.TelemetryCorrelation", "1.0.5", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Caching.Abstractions", "1.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Caching.Memory", "1.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.DiagnosticAdapter", "1.1.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.PlatformAbstractions", "1.1.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Moq", "4.16.1", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("MSTest.TestFramework", "2.1.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("System", "4.1.311.2", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("System.Buffers", "4.5.1", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Collections.Concurrent", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Collections.Immutable", "1.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Collections.NonGeneric", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Collections.Specialized", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.ComponentModel", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.ComponentModel.Primitives", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.ComponentModel.TypeConverter", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Core", "3.5.21022.801", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("System.Data.Common", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Diagnostics.PerformanceCounter", "4.5.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Diagnostics.StackTrace", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Dynamic.Runtime", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.IO.FileSystem.Primitives", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Linq", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Linq.Expressions", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Memory", "4.5.5", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Net.WebHeaderCollection", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.ObjectModel", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Private.DataContractSerialization", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Reflection.Emit", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Reflection.Emit.ILGeneration", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Reflection.Emit.Lightweight", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Reflection.Metadata", "1.4.1", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Reflection.TypeExtensions", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Runtime.InteropServices.RuntimeInformation", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Runtime.Numerics", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Runtime.Serialization.Json", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Runtime.Serialization.Primitives", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Security.Claims", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Security.Cryptography.OpenSsl", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Security.Cryptography.Primitives", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Security.Principal", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Text.RegularExpressions", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Threading", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Threading.Thread", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Threading.ThreadPool", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Xml.ReaderWriter", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Xml.XDocument", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Xml.XmlDocument", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Xml.XmlSerializer", "4.3.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.ApplicationInsights.Web", "2.10.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("MSTest.TestAdapter", "2.1.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), }; var packages = new[] { @@ -290,22 +290,22 @@ public async Task AllPackageDependencies_DoNotIncludeUpdateOnlyPackages() using var temp = new TemporaryDirectory(); var expectedDependencies = new Dependency[] { - new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown), - new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Buffers", "4.5.1", DependencyType.Unknown, IsTransitive: true), - new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Memory", "4.5.5", DependencyType.Unknown, IsTransitive: true), - new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, IsTransitive: true), - new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, IsTransitive: true), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.Bcl.AsyncInterfaces", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.DependencyInjection.Abstractions", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("Microsoft.Extensions.Logging", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Logging.Abstractions", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Options", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.Extensions.Primitives", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Buffers", "4.5.1", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.ComponentModel.Annotations", "5.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Diagnostics.DiagnosticSource", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Memory", "4.5.5", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), }; var packages = new[] { @@ -406,8 +406,8 @@ await File.WriteAllTextAsync( """); var expectedDependencies = new Dependency[] { - new("Newtonsoft.Json", "4.5.11", DependencyType.Unknown), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), + new("Newtonsoft.Json", "4.5.11", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), }; var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( temp.DirectoryPath, @@ -465,17 +465,17 @@ await File.WriteAllTextAsync( var expectedDependencies = new Dependency[] { - new("Microsoft.CodeAnalysis.Common", "4.8.0-3.23457.5", DependencyType.Unknown), - new("System.Buffers", "4.5.1", DependencyType.Unknown, IsTransitive: true), - new("System.Collections.Immutable", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Memory", "4.5.5", DependencyType.Unknown, IsTransitive: true), - new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, IsTransitive: true), - new("System.Reflection.Metadata", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Text.Encoding.CodePages", "7.0.0", DependencyType.Unknown, IsTransitive: true), - new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, IsTransitive: true), - new("Microsoft.CodeAnalysis.Analyzers", "3.3.4", DependencyType.Unknown, IsTransitive: true), - new("NETStandard.Library", "2.0.3", DependencyType.Unknown, IsTransitive: true), + new("Microsoft.CodeAnalysis.Common", "4.8.0-3.23457.5", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"]), + new("System.Buffers", "4.5.1", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Collections.Immutable", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Memory", "4.5.5", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Numerics.Vectors", "4.4.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Reflection.Metadata", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Runtime.CompilerServices.Unsafe", "6.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Text.Encoding.CodePages", "7.0.0", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("Microsoft.CodeAnalysis.Analyzers", "3.3.4", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), + new("NETStandard.Library", "2.0.3", DependencyType.Unknown, TargetFrameworks: ["netstandard2.0"], IsTransitive: true), }; var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( temp.DirectoryPath, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs index 4f4e0645904..9fcbf8072c4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs @@ -1,5 +1,7 @@ using System.Collections.Immutable; +using NuGetUpdater.Core.Utilities; + namespace NuGetUpdater.Core; public sealed record Dependency( @@ -12,4 +14,45 @@ public sealed record Dependency( bool IsDirect = false, bool IsTransitive = false, bool IsOverride = false, - bool IsUpdate = false); + bool IsUpdate = false) : IEquatable +{ + public bool Equals(Dependency? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Name == other.Name && + Version == other.Version && + Type == other.Type && + EvaluationResult == other.EvaluationResult && + TargetFrameworks.SequenceEqual(other.TargetFrameworks) && + IsDevDependency == other.IsDevDependency && + IsDirect == other.IsDirect && + IsTransitive == other.IsTransitive && + IsOverride == other.IsOverride && + IsUpdate == other.IsUpdate; + } + + public override int GetHashCode() + { + HashCode hash = new(); + hash.Add(Name); + hash.Add(Version); + hash.Add(Type); + hash.Add(EvaluationResult); + hash.Add(TargetFrameworks); + hash.Add(IsDevDependency); + hash.Add(IsDirect); + hash.Add(IsTransitive); + hash.Add(IsOverride); + hash.Add(IsUpdate); + return hash.ToHashCode(); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index 3b7191cb5b5..f49577889e1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -42,8 +42,9 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out { dotNetToolsJsonDiscovery = DotNetToolsJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); globalJsonDiscovery = GlobalJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); + ImmutableArray externalProperties = []; - projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath); + projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath, externalProperties); directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger); } @@ -68,7 +69,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out _processedProjectPaths.Clear(); } - private async Task> RunForDirectoryAsnyc(string repoRootPath, string workspacePath) + private async Task> RunForDirectoryAsnyc(string repoRootPath, string workspacePath, ImmutableArray externalProperties) { _logger.Log($"Running for directory [{Path.GetRelativePath(repoRootPath, workspacePath)}]"); var projectPaths = FindProjectFiles(workspacePath); @@ -78,7 +79,7 @@ private async Task> RunForDirectoryAsnyc( return []; } - return await RunForProjectPathsAsync(repoRootPath, workspacePath, projectPaths); + return await RunForProjectPathsAsync(repoRootPath, workspacePath, projectPaths, externalProperties); } private static ImmutableArray FindProjectFiles(string workspacePath) @@ -92,7 +93,7 @@ private static ImmutableArray FindProjectFiles(string workspacePath) .ToImmutableArray(); } - private async Task> RunForProjectPathsAsync(string repoRootPath, string workspacePath, IEnumerable projectPaths) + private async Task> RunForProjectPathsAsync(string repoRootPath, string workspacePath, IEnumerable projectPaths, ImmutableArray externalProperties) { var results = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var projectPath in projectPaths) @@ -113,7 +114,7 @@ private async Task> RunForProjectPathsAsy var packagesConfigDependencies = PackagesConfigDiscovery.Discover(workspacePath, projectPath, _logger) ?.Dependencies; - var projectResults = await SdkProjectDiscovery.DiscoverAsync(repoRootPath, workspacePath, projectPath, _logger); + var projectResults = await SdkProjectDiscovery.DiscoverAsync(repoRootPath, workspacePath, projectPath, externalProperties, _logger); foreach (var projectResult in projectResults) { if (results.ContainsKey(projectResult.FilePath)) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs index 12988d3f4c7..2b4b5bc8281 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs @@ -6,14 +6,14 @@ namespace NuGetUpdater.Core.Discover; internal static class SdkProjectDiscovery { - public static async Task> DiscoverAsync(string repoRootPath, string workspacePath, string projectPath, Logger logger) + public static async Task> DiscoverAsync(string repoRootPath, string workspacePath, string projectPath, ImmutableArray externalProperties, Logger logger) { // Determine which targets and props files contribute to the build. var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath); // Get all the dependencies which are directly referenced from the project file or indirectly referenced from // targets and props files. - var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles); + var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles, externalProperties); var results = ImmutableArray.CreateBuilder(); foreach (var buildFile in buildFiles) @@ -37,8 +37,8 @@ public static async Task> DiscoverAsync(s if (buildFile.GetFileType() == ProjectBuildFileType.Project) { // Collect information that is specific to the project file. - var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles).ToImmutableArray(); - var properties = MSBuildHelper.GetProperties(buildFiles).ToImmutableDictionary(); + var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles, externalProperties).ToImmutableArray(); + var properties = MSBuildHelper.GetProperties(buildFiles, externalProperties).ToImmutableDictionary(); var referencedProjectPaths = MSBuildHelper.GetProjectPathsFromProject(projectPath).ToImmutableArray(); // Get the complete set of dependencies including transitive dependencies. @@ -53,7 +53,8 @@ public static async Task> DiscoverAsync(s FilePath = Path.GetRelativePath(workspacePath, buildFile.Path), Properties = properties, TargetFrameworks = tfms, - ReferencedProjectPaths = referencedProjectPaths, + ReferencedProjectPaths = referencedProjectPaths + .Select(path => Path.GetRelativePath(workspacePath, path)).ToImmutableArray(), Dependencies = dependencies, }); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 8ad45c2d70a..858ec4d913a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -26,10 +26,10 @@ public static async Task UpdateDependencyAsync( logger.Log(" Running for SDK-style project"); var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath); - var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles); + var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles, externalProperties: []); // Get the set of all top-level dependencies in the current project - var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray(); + var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles, externalProperties: []).ToArray(); if (!await DoesDependencyRequireUpdateAsync(repoRootPath, projectPath, tfms, topLevelDependencies, dependencyName, newDependencyVersion, logger)) { return; @@ -564,7 +564,7 @@ private static IEnumerable FindPackageNodes( private static async Task AreDependenciesCoherentAsync(string repoRootPath, string projectPath, string dependencyName, Logger logger, ImmutableArray buildFiles, string[] tfms) { - var updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray(); + var updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles, []).ToArray(); foreach (var tfm in tfms) { var updatedPackages = await MSBuildHelper.GetAllPackageDependenciesAsync(repoRootPath, projectPath, tfm, updatedTopLevelDependencies, logger); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ImmutableArrayExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ImmutableArrayExtensions.cs new file mode 100644 index 00000000000..96de424d400 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ImmutableArrayExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Utilities; + +public static class ImmutableArrayExtensions +{ + public static bool SequenceEqual(this ImmutableArray? expected, ImmutableArray? actual, IEqualityComparer? equalityComparer = null) + { + if (expected is null) + { + return actual is null; + } + else + { + return actual is not null && expected.Value.SequenceEqual(actual.Value, equalityComparer); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 1b5a4684ebe..cdb099f73ec 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -40,11 +40,16 @@ public static void RegisterMSBuild() } } - public static string[] GetTargetFrameworkMonikers(ImmutableArray buildFiles) + public static string[] GetTargetFrameworkMonikers(ImmutableArray buildFiles, ImmutableArray externalProperties) { HashSet targetFrameworkValues = new(StringComparer.OrdinalIgnoreCase); Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); + foreach (var property in externalProperties) + { + propertyInfo[property.Name] = property; + } + foreach (var buildFile in buildFiles) { var projectRoot = CreateProjectRootElement(buildFile); @@ -163,10 +168,15 @@ public static IEnumerable GetProjectPathsFromProject(string projFilePath } } - public static IReadOnlyDictionary GetProperties(ImmutableArray buildFiles) + public static IReadOnlyDictionary GetProperties(ImmutableArray buildFiles, ImmutableArray externalProperties) { Dictionary properties = new(StringComparer.OrdinalIgnoreCase); + foreach (var property in externalProperties) + { + properties[property.Name] = property; + } + foreach (var buildFile in buildFiles) { var projectRoot = CreateProjectRootElement(buildFile); @@ -189,12 +199,17 @@ public static IReadOnlyDictionary GetProperties(ImmutableArray return properties; } - public static IEnumerable GetTopLevelPackageDependencyInfos(ImmutableArray buildFiles) + public static IEnumerable GetTopLevelPackageDependencyInfos(ImmutableArray buildFiles, ImmutableArray externalProperties) { Dictionary packageInfo = new(StringComparer.OrdinalIgnoreCase); Dictionary packageVersionInfo = new(StringComparer.OrdinalIgnoreCase); Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); + foreach (var property in externalProperties) + { + propertyInfo[property.Name] = property; + } + foreach (var buildFile in buildFiles) { var projectRoot = CreateProjectRootElement(buildFile); From 996074927ef0f2ca7e1c82bef5e47f37f501daec Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sat, 23 Mar 2024 00:19:11 -0700 Subject: [PATCH 15/26] Parse MSBuild SDKs, Restore MSBuild SDKs, Read Properties from all build files --- nuget/Dockerfile | 3 +- .../EntryPointTests.Discover.cs | 27 +- .../Discover/DiscoveryWorkerTestBase.cs | 17 +- .../DiscoveryWorkerTests.DotNetToolsJson.cs | 91 ++++++ .../DiscoveryWorkerTests.GlobalJson.cs | 71 +++++ .../DiscoveryWorkerTests.PackagesConfig.cs | 59 ++++ .../Discover/DiscoveryWorkerTests.Project.cs | 296 ++++++++++++++++++ .../Discover/DiscoveryWorkerTests.cs | 69 ++-- .../Discover/ExpectedDiscoveryResults.cs | 25 +- .../Files/GlobalJsonBuildFileTests.cs | 1 + .../Files/PackagesConfigBuildFileTests.cs | 6 +- .../Files/ProjectBuildFileTests.cs | 7 +- .../Utilities/MSBuildHelperTests.cs | 30 +- .../NuGetUpdater.Core/DependencyType.cs | 2 +- .../DirectoryPackagesPropsDiscovery.cs | 32 +- .../DirectoryPackagesPropsDiscoveryResult.cs | 1 + .../Discover/DiscoveryWorker.cs | 80 ++++- .../Discover/DotNetToolsJsonDiscovery.cs | 7 +- .../DotNetToolsJsonDiscoveryResult.cs | 1 + .../Discover/GlobalJsonDiscovery.cs | 7 +- .../Discover/GlobalJsonDiscoveryResult.cs | 1 + .../Discover/IDiscoveryResult.cs | 1 + .../Discover/PackagesConfigDiscovery.cs | 6 +- .../Discover/PackagesConfigDiscoveryResult.cs | 1 + .../Discover/ProjectDiscoveryResult.cs | 5 +- .../Discover/SdkProjectDiscovery.cs | 41 ++- .../Discover/WorkspaceDiscoveryResult.cs | 1 + .../NuGetUpdater.Core/EvaluationResult.cs | 3 +- .../NuGetUpdater.Core/Files/BuildFile.cs | 2 + .../Files/GlobalJsonBuildFile.cs | 26 +- .../NuGetUpdater.Core/Files/JsonBuildFile.cs | 1 + .../Files/PackagesConfigBuildFile.cs | 2 +- .../Files/ProjectBuildFile.cs | 78 ++++- .../Updater/SdkPackageUpdater.cs | 11 +- .../Utilities/HashSetExtensions.cs | 14 + .../Utilities/MSBuildHelper.cs | 115 ++++--- .../Utilities/NuGetHelper.cs | 16 + 37 files changed, 957 insertions(+), 199 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs diff --git a/nuget/Dockerfile b/nuget/Dockerfile index 1700914b4d3..bbe9403c8d7 100644 --- a/nuget/Dockerfile +++ b/nuget/Dockerfile @@ -24,7 +24,8 @@ RUN cd /tmp \ && chmod +x dotnet-install.sh \ && mkdir -p "${DOTNET_INSTALL_DIR}" \ && ./dotnet-install.sh --version "${DOTNET_SDK_VERSION}" --install-dir "${DOTNET_INSTALL_DIR}" \ - && rm dotnet-install.sh + && rm dotnet-install.sh \ + && chown -R dependabot:dependabot "${DOTNET_INSTALL_DIR}/sdk" ENV PATH="${PATH}:${DOTNET_INSTALL_DIR}" RUN dotnet --list-runtimes diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index 09fdea7fada..2ccd2458e40 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -86,12 +86,11 @@ await RunAsync(path => ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig, TargetFrameworks: ["net45"]), + new("Newtonsoft.Json", "7.0.1", DependencyType.PackagesConfig, TargetFrameworks: ["net45"]), + ], + Properties = [ + new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), ], - Properties = new Dictionary() - { - ["TargetFrameworkVersion"] = new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), - }.ToImmutableDictionary() } ] }); @@ -146,12 +145,11 @@ await RunAsync(path => ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig, TargetFrameworks: ["net45"]) + new("Newtonsoft.Json", "7.0.1", DependencyType.PackagesConfig, TargetFrameworks: ["net45"]) + ], + Properties = [ + new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), ], - Properties = new Dictionary() - { - ["TargetFrameworkVersion"] = new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), - }.ToImmutableDictionary() } ] }); @@ -206,12 +204,11 @@ await RunAsync(path => ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig, TargetFrameworks: ["net45"]) + new("Newtonsoft.Json", "7.0.1", DependencyType.PackagesConfig, TargetFrameworks: ["net45"]) + ], + Properties = [ + new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), ], - Properties = new Dictionary() - { - ["TargetFrameworkVersion"] = new("TargetFrameworkVersion", "v4.5", "path/to/my.csproj"), - }.ToImmutableDictionary() } ] }); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index ab194782b23..c1476d28d3b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -12,7 +12,7 @@ namespace NuGetUpdater.Core.Test.Discover; public class DiscoveryWorkerTestBase { - protected static async Task TestDiscovery( + protected static async Task TestDiscoveryAsync( string workspacePath, TestFile[] files, ExpectedWorkspaceDiscoveryResult expectedResult) @@ -38,7 +38,7 @@ protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult e return; - void ValidateResultWithDependencies(IDiscoveryResultWithDependencies? expectedResult, IDiscoveryResultWithDependencies? actualResult) + void ValidateResultWithDependencies(ExpectedDependencyDiscoveryResult? expectedResult, IDiscoveryResultWithDependencies? actualResult) { if (expectedResult is null) { @@ -52,10 +52,16 @@ void ValidateResultWithDependencies(IDiscoveryResultWithDependencies? expectedRe Assert.Equal(expectedResult.FilePath, actualResult.FilePath); ValidateDependencies(expectedResult.Dependencies, actualResult.Dependencies); + Assert.Equal(expectedResult.ExpectedDependencyCount ?? expectedResult.Dependencies.Length, actualResult.Dependencies.Length); } void ValidateProjectResults(ImmutableArray expectedProjects, ImmutableArray actualProjects) { + if (expectedProjects.IsDefaultOrEmpty) + { + return; + } + foreach (var expectedProject in expectedProjects) { var actualProject = actualProjects.Single(p => p.FilePath == expectedProject.FilePath); @@ -69,7 +75,7 @@ void ValidateProjectResults(ImmutableArray ex } } - void ValidateDirectoryPackagesProps(DirectoryPackagesPropsDiscoveryResult? expected, DirectoryPackagesPropsDiscoveryResult? actual) + void ValidateDirectoryPackagesProps(ExpectedDirectoryPackagesPropsDiscovertyResult? expected, DirectoryPackagesPropsDiscoveryResult? actual) { ValidateResultWithDependencies(expected, actual); Assert.Equal(expected?.IsTransitivePinningEnabled, actual?.IsTransitivePinningEnabled); @@ -77,6 +83,11 @@ void ValidateDirectoryPackagesProps(DirectoryPackagesPropsDiscoveryResult? expec void ValidateDependencies(ImmutableArray expectedDependencies, ImmutableArray actualDependencies) { + if (expectedDependencies.IsDefault) + { + return; + } + foreach (var expectedDependency in expectedDependencies) { var actualDependency = actualDependencies.Single(d => d.Name == expectedDependency.Name); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs new file mode 100644 index 00000000000..ce0cffdda4c --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs @@ -0,0 +1,91 @@ +using Xunit; + +namespace NuGetUpdater.Core.Test.Discover; + +public partial class DiscoveryWorkerTests +{ + public class DotNetToolsJson : DiscoveryWorkerTestBase + { + [Fact] + public async Task DiscoversDependencies() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + (".config/dotnet-tools.json", """ + { + "version": 1, + "isRoot": true, + "tools": { + "botsay": { + "version": "1.0.0", + "commands": [ + "botsay" + ] + }, + "dotnetsay": { + "version": "1.0.0", + "commands": [ + "dotnetsay" + ] + } + } + } + """), + ], + expectedResult: new() + { + FilePath = "", + DotNetToolsJson = new() + { + FilePath = ".config/dotnet-tools.json", + Dependencies = [ + new("botsay", "1.0.0", DependencyType.DotNetTool), + new("dotnetsay", "1.0.0", DependencyType.DotNetTool), + ] + }, + ExpectedProjectCount = 0, + }); + } + + [Fact] + public async Task ReportsFailure() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + (".config/dotnet-tools.json", """ + { + "version": 1, + "isRoot": true, + "tools": { + "botsay": { + "version": "1.0.0", + "commands": [ + "botsay" + ], + }, + "dotnetsay": { + "version": "1.0.0", + "commands": [ + "dotnetsay" + ] + } + } + } + """), + ], + expectedResult: new() + { + FilePath = "", + DotNetToolsJson = new() + { + FilePath = ".config/dotnet-tools.json", + IsSuccess = false, + ExpectedDependencyCount = 0, + }, + ExpectedProjectCount = 0, + }); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs new file mode 100644 index 00000000000..dd0b0ee8bac --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs @@ -0,0 +1,71 @@ +using Xunit; + +namespace NuGetUpdater.Core.Test.Discover; + +public partial class DiscoveryWorkerTests +{ + public class GlobalJson : DiscoveryWorkerTestBase + { + [Fact] + public async Task DiscoversDependencies() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + ("global.json", """ + { + "sdk": { + "version": "2.2.104" + }, + "msbuild-sdks": { + "Microsoft.Build.Traversal": "1.0.45" + } + } + """), + ], + expectedResult: new() + { + FilePath = "", + GlobalJson = new() + { + FilePath = "global.json", + Dependencies = [ + new("Microsoft.NET.Sdk", "2.2.104", DependencyType.MSBuildSdk), + new("Microsoft.Build.Traversal", "1.0.45", DependencyType.MSBuildSdk), + ] + }, + ExpectedProjectCount = 0, + }); + } + + [Fact] + public async Task ReportsFailure() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + ("global.json", """ + { + "sdk": { + "version": "2.2.104", + }, + "msbuild-sdks": { + "Microsoft.Build.Traversal": "1.0.45" + } + } + """), + ], + expectedResult: new() + { + FilePath = "", + GlobalJson = new() + { + FilePath = "global.json", + IsSuccess = false, + ExpectedDependencyCount = 0, + }, + ExpectedProjectCount = 0, + }); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs new file mode 100644 index 00000000000..035da3a873b --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs @@ -0,0 +1,59 @@ +using System.Collections.Immutable; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Discover; + +public partial class DiscoveryWorkerTests +{ + public class PackagesConfig : DiscoveryWorkerTestBase + { + [Fact] + public async Task DiscoversDependencies() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + ("packages.config", """ + + + + + + + + + + + + + """), + ("myproj.csproj", """ + + + """) + ], + expectedResult: new() + { + FilePath = "", + Projects = [ + new() + { + FilePath = "myproj.csproj", + Dependencies = [ + new("Microsoft.CodeDom.Providers.DotNetCompilerPlatform", "1.0.0", DependencyType.PackagesConfig, TargetFrameworks: []), + new("Microsoft.Net.Compilers", "1.0.1", DependencyType.PackagesConfig, TargetFrameworks: []), + new("Microsoft.Web.Infrastructure", "1.0.0.0", DependencyType.PackagesConfig, TargetFrameworks: []), + new("Microsoft.Web.Xdt", "2.1.1", DependencyType.PackagesConfig, TargetFrameworks: []), + new("Newtonsoft.Json", "8.0.3", DependencyType.PackagesConfig, TargetFrameworks: []), + new("NuGet.Core", "2.11.1", DependencyType.PackagesConfig, TargetFrameworks: []), + new("NuGet.Server", "2.11.2", DependencyType.PackagesConfig, TargetFrameworks: []), + new("RouteMagic", "1.3", DependencyType.PackagesConfig, TargetFrameworks: []), + new("WebActivatorEx", "2.1.0", DependencyType.PackagesConfig, TargetFrameworks: []), + ], + } + ], + }); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs new file mode 100644 index 00000000000..1ebfa677b82 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs @@ -0,0 +1,296 @@ +using Xunit; + +namespace NuGetUpdater.Core.Test.Discover; + +public partial class DiscoveryWorkerTests +{ + public class Projects : DiscoveryWorkerTestBase + { + [Fact] + public async Task ReturnsPackageReferencesMissingVersions() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + ("myproj.csproj", """ + + + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + netstandard1.6;net462 + + + + + + + + + + + + 4.3.0 + + + + + + + """) + ], + expectedResult: new() + { + FilePath = "", + Projects = [ + new() + { + FilePath = "myproj.csproj", + ExpectedDependencyCount = 52, + Dependencies = [ + new("Microsoft.Extensions.DependencyModel", "1.1.1", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.AspNetCore.App", "", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.NET.Test.Sdk", "", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), + new("Microsoft.Extensions.PlatformAbstractions", "1.1.0", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("System.Collections.Specialized", "4.3.0", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + ], + Properties = [ + new("Description", "Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications.", "myproj.csproj"), + new("TargetFrameworks", "netstandard1.6;net462", "myproj.csproj"), + ], + TargetFrameworks = ["net462", "netstandard1.6"], + ReferencedProjectPaths = [], + } + ], + }); + } + + [Fact] + public async Task WithDirectoryPackagesProps() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + ("myproj.csproj", """ + + + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + netstandard1.6;net462 + + + + + + + + + + + + 4.3.0 + + + + + + + """), + ("directory.packages.props", """ + + + true + + + + + + + + + """), + ], + expectedResult: new() + { + FilePath = "", + Projects = [ + new() + { + FilePath = "myproj.csproj", + ExpectedDependencyCount = 52, + Dependencies = [ + new("Microsoft.Extensions.DependencyModel", "1.1.1", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.AspNetCore.App", "", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.NET.Test.Sdk", "", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), + new("Microsoft.Extensions.PlatformAbstractions", "1.1.0", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("System.Collections.Specialized", "4.3.0", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + ], + Properties = [ + new("Description", "Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications.", "myproj.csproj"), + new("ManagePackageVersionsCentrally", "true", "Directory.Packages.props"), + new("TargetFrameworks", "netstandard1.6;net462", "myproj.csproj"), + ], + TargetFrameworks = ["net462", "netstandard1.6"], + }, + ], + DirectoryPackagesProps = new() + { + FilePath = "Directory.Packages.props", + Dependencies = [ + new("System.Lycos", "3.23.3", DependencyType.PackageVersion, IsDirect: true), + new("System.AskJeeves", "2.2.2", DependencyType.PackageVersion, IsDirect: true), + new("System.Google", "0.1.0-beta.3", DependencyType.PackageVersion, IsDirect: true), + new("System.WebCrawler", "1.1.1", DependencyType.PackageVersion, IsDirect: true), + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), + ], + }, + }); + } + + [Fact] + public async Task WithPackagesProps() + { + var nugetPackagesDirectory = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); + var nugetHttpCacheDirectory = Environment.GetEnvironmentVariable("NUGET_HTTP_CACHE_PATH"); + + try + { + using var temp = new TemporaryDirectory(); + + // It is important to have empty NuGet caches for this test, so override them with temp directories. + var tempNuGetPackagesDirectory = Path.Combine(temp.DirectoryPath, ".nuget", "packages"); + Environment.SetEnvironmentVariable("NUGET_PACKAGES", tempNuGetPackagesDirectory); + var tempNuGetHttpCacheDirectory = Path.Combine(temp.DirectoryPath, ".nuget", "v3-cache"); + Environment.SetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", tempNuGetHttpCacheDirectory); + + await TestDiscoveryAsync( + workspacePath: "", + files: [ + ("myproj.csproj", """ + + + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + netstandard1.6;net462 + + + + + + + + + + + + 4.3.0 + + + + + + + """), + ("packages.props", """ + + + + + + + + + + + """), + ("Directory.Build.targets", """ + + + + """), + ], + expectedResult: new() + { + FilePath = "", + ExpectedProjectCount = 3, + Projects = [ + new() + { + FilePath = "myproj.csproj", + ExpectedDependencyCount = 52, + Dependencies = [ + new("Microsoft.Extensions.DependencyModel", "1.1.1", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.AspNetCore.App", "", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.NET.Test.Sdk", "", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), + new("Microsoft.Extensions.PlatformAbstractions", "1.1.0", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + new("System.Collections.Specialized", "4.3.0", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), + ], + Properties = [ + new("Description", "Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications.", "myproj.csproj"), + new("TargetFrameworks", "netstandard1.6;net462", "myproj.csproj"), + ], + TargetFrameworks = ["net462", "netstandard1.6"], + }, + new() + { + FilePath = "Packages.props", + Dependencies = [ + new("Microsoft.SourceLink.GitHub", "1.0.0-beta2-19367-01", DependencyType.GlobalPackageReference, IsDirect: true), + new("System.Lycos", "3.23.3", DependencyType.PackageReference, IsDirect: true, IsUpdate: true), + new("System.AskJeeves", "2.2.2", DependencyType.PackageReference, IsDirect: true, IsUpdate: true), + new("System.Google", "0.1.0-beta.3", DependencyType.PackageReference, IsDirect: true, IsUpdate: true), + new("System.WebCrawler", "1.1.1", DependencyType.PackageReference, IsDirect: true, IsUpdate: true), + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), + ], + }, + ], + }); + } + finally + { + // Restore the NuGet caches. + Environment.SetEnvironmentVariable("NUGET_PACKAGES", nugetPackagesDirectory); + Environment.SetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", nugetHttpCacheDirectory); + } + } + + [Fact] + public async Task ReturnsDependenciesThatCannotBeEvaluated() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + ("myproj.csproj", """ + + + net8.0 + + + + + + + """) + ], + expectedResult: new() + { + FilePath = "", + Projects = [ + new() + { + FilePath = "myproj.csproj", + Dependencies = [ + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), + new("Package.A", "1.2.3", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), + new("Package.B", "$(ThisPropertyCannotBeResolved)", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), + ], + Properties = [ + new("TargetFramework", "net8.0", "myproj.csproj"), + ], + TargetFrameworks = ["net8.0"], + ReferencedProjectPaths = [], + } + ], + }); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs index 08544863cf2..9351ca51aea 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -1,10 +1,8 @@ -using System.Collections.Immutable; - using Xunit; namespace NuGetUpdater.Core.Test.Discover; -public class DiscoveryWorkerTests : DiscoveryWorkerTestBase +public partial class DiscoveryWorkerTests : DiscoveryWorkerTestBase { [Theory] [InlineData("src/project.csproj")] @@ -12,7 +10,7 @@ public class DiscoveryWorkerTests : DiscoveryWorkerTestBase [InlineData("src/project.fsproj")] public async Task TestProjectFiles(string projectPath) { - await TestDiscovery( + await TestDiscoveryAsync( workspacePath: "src", files: new[] { @@ -38,15 +36,15 @@ await TestDiscovery( FilePath = Path.GetFileName(projectPath), TargetFrameworks = ["netstandard2.0"], ReferencedProjectPaths = [], - ExpectedDependencyCount = 18, + ExpectedDependencyCount = 19, Dependencies = [ + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, TargetFrameworks: ["netstandard2.0"], IsDirect: true) ], - Properties = new Dictionary() - { - ["NewtonsoftJsonPackageVersion"] = new("NewtonsoftJsonPackageVersion", "9.0.1", projectPath), - ["TargetFramework"] = new("TargetFramework", "netstandard2.0", projectPath), - }.ToImmutableDictionary() + Properties = [ + new("NewtonsoftJsonPackageVersion", "9.0.1", projectPath), + new("TargetFramework", "netstandard2.0", projectPath), + ] } ] } @@ -57,7 +55,7 @@ await TestDiscovery( public async Task TestPackageConfig() { var projectPath = "src/project.csproj"; - await TestDiscovery( + await TestDiscoveryAsync( workspacePath: "", files: new[] { @@ -96,12 +94,11 @@ await TestDiscovery( ReferencedProjectPaths = [], ExpectedDependencyCount = 2, // Should we ignore Microsoft.NET.ReferenceAssemblies? Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageConfig, TargetFrameworks: ["net45"]) + new("Newtonsoft.Json", "7.0.1", DependencyType.PackagesConfig, TargetFrameworks: ["net45"]) ], - Properties = new Dictionary() - { - ["TargetFrameworkVersion"] = new("TargetFrameworkVersion", "v4.5", projectPath), - }.ToImmutableDictionary() + Properties = [ + new("TargetFrameworkVersion", "v4.5", projectPath), + ] } ] } @@ -112,7 +109,7 @@ await TestDiscovery( public async Task TestProps() { var projectPath = "src/project.csproj"; - await TestDiscovery( + await TestDiscoveryAsync( workspacePath: "", files: new[] { @@ -143,23 +140,22 @@ await TestDiscovery( expectedResult: new() { FilePath = "", - ExpectedProjectCount = 2, Projects = [ new() { FilePath = projectPath, TargetFrameworks = ["netstandard2.0"], ReferencedProjectPaths = [], - ExpectedDependencyCount = 18, + ExpectedDependencyCount = 19, Dependencies = [ + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, TargetFrameworks: ["netstandard2.0"], IsDirect: true) ], - Properties = new Dictionary() - { - ["ManagePackageVersionsCentrally"] = new("ManagePackageVersionsCentrally", "true", "Directory.Packages.props"), - ["NewtonsoftJsonPackageVersion"] = new("NewtonsoftJsonPackageVersion", "9.0.1", "Directory.Packages.props"), - ["TargetFramework"] = new("TargetFramework", "netstandard2.0", projectPath), - }.ToImmutableDictionary() + Properties = [ + new("ManagePackageVersionsCentrally", "true", "Directory.Packages.props"), + new("NewtonsoftJsonPackageVersion", "9.0.1", "Directory.Packages.props"), + new("TargetFramework", "netstandard2.0", projectPath), + ] } ], DirectoryPackagesProps = new() @@ -177,7 +173,7 @@ await TestDiscovery( public async Task TestRepo() { var solutionPath = "solution.sln"; - await TestDiscovery( + await TestDiscoveryAsync( workspacePath: "", files: new[] { @@ -263,23 +259,21 @@ await TestDiscovery( expectedResult: new() { FilePath = "", - ExpectedProjectCount = 2, Projects = [ new() { FilePath = "src/project.csproj", - TargetFrameworks = ["netstandard2.0", "net6.0"], - ReferencedProjectPaths = [], - ExpectedDependencyCount = 18, + TargetFrameworks = ["net6.0", "netstandard2.0"], + ExpectedDependencyCount = 19, Dependencies = [ - new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, TargetFrameworks: ["netstandard2.0", "net6.0"], IsDirect: true) + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), + new("Newtonsoft.Json", "9.0.1", DependencyType.PackageReference, TargetFrameworks: ["net6.0", "netstandard2.0"], IsDirect: true) ], - Properties = new Dictionary() - { - ["ManagePackageVersionsCentrally"] = new("ManagePackageVersionsCentrally", "true", "Directory.Packages.props"), - ["NewtonsoftJsonPackageVersion"] = new("NewtonsoftJsonPackageVersion", "9.0.1", "Directory.Packages.props"), - ["TargetFrameworks"] = new("TargetFrameworks", "netstandard2.0;net6.0", "src/project.csproj"), - }.ToImmutableDictionary() + Properties = [ + new("ManagePackageVersionsCentrally", "true", "Directory.Packages.props"), + new("NewtonsoftJsonPackageVersion", "9.0.1", "Directory.Packages.props"), + new("TargetFrameworks", "netstandard2.0;net6.0", "src/project.csproj"), + ] } ], DirectoryPackagesProps = new() @@ -293,6 +287,7 @@ await TestDiscovery( { FilePath = "global.json", Dependencies = [ + new("Microsoft.NET.Sdk", "6.0.405", DependencyType.MSBuildSdk), new("My.Custom.Sdk", "5.0.0", DependencyType.MSBuildSdk), new("My.Other.Sdk", "1.0.0-beta", DependencyType.MSBuildSdk), ] diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs index ccd5c58bae6..4fd2094d7b2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs @@ -7,19 +7,30 @@ namespace NuGetUpdater.Core.Test.Discover; public record ExpectedWorkspaceDiscoveryResult : IDiscoveryResult { public required string FilePath { get; init; } + public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public int? ExpectedProjectCount { get; init; } - public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } - public GlobalJsonDiscoveryResult? GlobalJson { get; init; } - public DotNetToolsJsonDiscoveryResult? DotNetToolsJson { get; init; } + public ExpectedDirectoryPackagesPropsDiscovertyResult? DirectoryPackagesProps { get; init; } + public ExpectedDependencyDiscoveryResult? GlobalJson { get; init; } + public ExpectedDependencyDiscoveryResult? DotNetToolsJson { get; init; } } -public record ExpectedSdkProjectDiscoveryResult : IDiscoveryResultWithDependencies +public record ExpectedDirectoryPackagesPropsDiscovertyResult : ExpectedDependencyDiscoveryResult +{ + public bool IsTransitivePinningEnabled { get; init; } +} + +public record ExpectedSdkProjectDiscoveryResult : ExpectedDependencyDiscoveryResult +{ + public ImmutableArray Properties { get; init; } = []; + public ImmutableArray TargetFrameworks { get; init; } = []; + public ImmutableArray ReferencedProjectPaths { get; init; } = []; +} + +public record ExpectedDependencyDiscoveryResult : IDiscoveryResultWithDependencies { public required string FilePath { get; init; } - public required ImmutableDictionary Properties { get; init; } - public ImmutableArray TargetFrameworks { get; init; } - public ImmutableArray ReferencedProjectPaths { get; init; } + public bool IsSuccess { get; init; } = true; public ImmutableArray Dependencies { get; init; } public int? ExpectedDependencyCount { get; init; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs index 0d2873fd13b..c8d79014cc2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs @@ -53,6 +53,7 @@ public void GlobalJson_GetDependencies_ReturnsDependencies() { var expectedDependencies = new List { + new("Microsoft.NET.Sdk", "6.0.405", DependencyType.MSBuildSdk), new("My.Custom.Sdk", "5.0.0", DependencyType.MSBuildSdk), new("My.Other.Sdk", "1.0.0-beta", DependencyType.MSBuildSdk) }; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs index fbc1fda5173..ea84654efae 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs @@ -35,9 +35,9 @@ public void PackagesConfig_GetDependencies_ReturnsDependencies() { var expectedDependencies = new List { - new("Microsoft.CodeDom.Providers.DotNetCompilerPlatform", "1.0.0", DependencyType.PackageConfig), - new("Microsoft.Net.Compilers", "1.0.0", DependencyType.PackageConfig, IsDevDependency: true), - new("Newtonsoft.Json", "8.0.3", DependencyType.PackageConfig) + new("Microsoft.CodeDom.Providers.DotNetCompilerPlatform", "1.0.0", DependencyType.PackagesConfig), + new("Microsoft.Net.Compilers", "1.0.0", DependencyType.PackagesConfig, IsDevDependency: true), + new("Newtonsoft.Json", "8.0.3", DependencyType.PackagesConfig) }; var buildFile = GetBuildFile(PackagesConfig); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs index 7803dda83c5..d95830a0311 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs @@ -3,6 +3,8 @@ using Microsoft.Language.Xml; +using NuGetUpdater.Core.Test.Utilities; + using Xunit; namespace NuGetUpdater.Core.Test.Files; @@ -57,16 +59,17 @@ public void ProjectCsProj_GetDependencies_ReturnsDependencies() { var expectedDependencies = new List { + new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk), new("GuiLabs.Language.Xml", "1.2.60", DependencyType.PackageReference), new("Microsoft.CodeAnalysis.CSharp", null, DependencyType.PackageReference), - new("Newtonsoft.Json", "13.0.3", DependencyType.PackageReference, IsOverride: true) + new("Newtonsoft.Json", "13.0.3", DependencyType.PackageReference, IsUpdate: true, IsOverride: true) }; var buildFile = GetBuildFile(ProjectCsProj, "Project.csproj"); var dependencies = buildFile.GetDependencies(); - Assert.Equal(expectedDependencies, dependencies); + AssertEx.Equal(expectedDependencies, dependencies); } [Fact] diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs index 736770fa937..96de0f1e921 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -33,7 +33,7 @@ public void GetRootedValue_FindsValue() }; // Act - var (resultType, _, evaluatedValue, _, _, _) = MSBuildHelper.GetEvaluatedValue(projectContents, propertyInfo); + var (resultType, _, evaluatedValue, _, _) = MSBuildHelper.GetEvaluatedValue(projectContents, propertyInfo); Assert.Equal(EvaluationResultType.Success, resultType); @@ -73,7 +73,7 @@ public async Task GetRootedValue_DoesNotRecurseAsync() await Task.Delay(1); // Act - var (resultType, _, _, _, _, errorMessage) = MSBuildHelper.GetEvaluatedValue(projectContents, propertyInfo); + var (resultType, _, _, _, errorMessage) = MSBuildHelper.GetEvaluatedValue(projectContents, propertyInfo); // Assert Assert.Equal(EvaluationResultType.CircularReference, resultType); @@ -118,7 +118,7 @@ public void TfmsCanBeDeterminedFromProjectContents(string projectContents, strin File.WriteAllText(projectPath, projectContents); var expectedTfms = new[] { expectedTfm1, expectedTfm2 }.Where(tfm => tfm is not null).ToArray(); var buildFile = ProjectBuildFile.Open(Path.GetDirectoryName(projectPath)!, projectPath); - var actualTfms = MSBuildHelper.GetTargetFrameworkMonikers(ImmutableArray.Create(buildFile), []); + var actualTfms = MSBuildHelper.GetTargetFrameworkMonikers(ImmutableArray.Create(buildFile)); AssertEx.Equal(expectedTfms, actualTfms); } finally @@ -140,7 +140,7 @@ public async Task TopLevelPackageDependenciesCanBeDetermined(TestFile[] buildFil buildFiles.Add(ProjectBuildFile.Parse(testDirectory.DirectoryPath, fullPath, content)); } - var actualTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles.ToImmutableArray(), []); + var actualTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles.ToImmutableArray()); AssertEx.Equal(expectedTopLevelDependencies, actualTopLevelDependencies); } @@ -516,7 +516,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Newtonsoft.Json", "12.0.1", DependencyType.Unknown, - EvaluationResult: new(EvaluationResultType.Success, "12.0.1", "12.0.1", null, null, null)) + EvaluationResult: new(EvaluationResultType.Success, "12.0.1", "12.0.1", null, null)) } ]; @@ -543,7 +543,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Newtonsoft.Json", "12.0.1", DependencyType.Unknown, - EvaluationResult: new(EvaluationResultType.Success, "12.0.1", "12.0.1", null, null, null)) + EvaluationResult: new(EvaluationResultType.Success, "12.0.1", "12.0.1", null, null)) } ]; @@ -571,7 +571,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Newtonsoft.Json", "12.0.1", DependencyType.Unknown, - new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } ]; @@ -601,7 +601,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Newtonsoft.Json", "12.0.1", DependencyType.Unknown, - new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } ]; @@ -631,7 +631,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Newtonsoft.Json", "12.0.1", DependencyType.Unknown, - new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } }; @@ -661,7 +661,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Newtonsoft.Json", "12.0.1", DependencyType.Unknown, - new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } ]; @@ -691,7 +691,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Newtonsoft.Json", "12.0.1", DependencyType.Unknown, - new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", "NewtonsoftJsonVersion", null)) + new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } }; @@ -727,12 +727,12 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Azure.Identity", "1.6.0", DependencyType.Unknown, - EvaluationResult: new(EvaluationResultType.Success, "1.6.0", "1.6.0", null, null, null)), + EvaluationResult: new(EvaluationResultType.Success, "1.6.0", "1.6.0", null, null)), new( "Microsoft.Data.SqlClient", "5.1.4", DependencyType.Unknown, - EvaluationResult: new(EvaluationResultType.Success, "5.1.4", "5.1.4", null, null, null), + EvaluationResult: new(EvaluationResultType.Success, "5.1.4", "5.1.4", null, null), IsUpdate: true), } ]; @@ -769,12 +769,12 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() "Azure.Identity", "1.6.0", DependencyType.Unknown, - EvaluationResult: new(EvaluationResultType.Success, "1.6.0", "1.6.0", null, null, null)), + EvaluationResult: new(EvaluationResultType.Success, "1.6.0", "1.6.0", null, null)), new( "Microsoft.Data.SqlClient", "5.1.4", DependencyType.Unknown, - EvaluationResult: new(EvaluationResultType.Success, "5.1.4", "5.1.4", null, null, null), + EvaluationResult: new(EvaluationResultType.Success, "5.1.4", "5.1.4", null, null), IsUpdate: true), } ]; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyType.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyType.cs index 16128ffcd7d..1431f70264c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyType.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyType.cs @@ -3,7 +3,7 @@ namespace NuGetUpdater.Core; public enum DependencyType { Unknown, - PackageConfig, + PackagesConfig, PackageReference, PackageVersion, GlobalPackageReference, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs index 6777690b7af..619ac84860e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs @@ -7,7 +7,7 @@ internal static class DirectoryPackagesPropsDiscovery public static DirectoryPackagesPropsDiscoveryResult? Discover(string repoRootPath, string workspacePath, ImmutableArray projectResults, Logger logger) { var projectResult = projectResults.FirstOrDefault( - p => p.Properties.TryGetValue("ManagePackageVersionsCentrally", out var property) + p => p.Properties.FirstOrDefault(prop => prop.Name.Equals("ManagePackageVersionsCentrally", StringComparison.OrdinalIgnoreCase)) is Property property && string.Equals(property.Value, "true", StringComparison.OrdinalIgnoreCase)); if (projectResult is null) { @@ -23,7 +23,7 @@ internal static class DirectoryPackagesPropsDiscovery } var relativeDirectoryPackagesPropsPath = Path.GetRelativePath(workspacePath, directoryPackagesPropsPath); - var directoryPackagesPropsFile = projectResults.FirstOrDefault(p => p.FilePath == relativeDirectoryPackagesPropsPath); + var directoryPackagesPropsFile = projectResults.FirstOrDefault(p => p.FilePath.Equals(relativeDirectoryPackagesPropsPath, StringComparison.OrdinalIgnoreCase)); if (directoryPackagesPropsFile is null) { logger.Log($" No project file found for [{relativeDirectoryPackagesPropsPath}]."); @@ -32,14 +32,38 @@ internal static class DirectoryPackagesPropsDiscovery logger.Log($" Discovered [{directoryPackagesPropsFile.FilePath}] file."); - var isTransitivePinningEnabled = projectResult.Properties.TryGetValue("EnableTransitivePinning", out var property) + var isTransitivePinningEnabled = projectResult.Properties.FirstOrDefault(prop => prop.Name.Equals("EnableTransitivePinning", StringComparison.OrdinalIgnoreCase)) is Property property && string.Equals(property.Value, "true", StringComparison.OrdinalIgnoreCase); + var properties = projectResult.Properties.ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); + var dependencies = GetDependencies(workspacePath, directoryPackagesPropsPath, properties) + .OrderBy(d => d.Name) + .ToImmutableArray(); return new() { FilePath = directoryPackagesPropsFile.FilePath, IsTransitivePinningEnabled = isTransitivePinningEnabled, - Dependencies = directoryPackagesPropsFile.Dependencies, + Dependencies = dependencies, }; } + + private static IEnumerable GetDependencies(string workspacePath, string directoryPackagesPropsPath, ImmutableDictionary properties) + { + var dependencies = ProjectBuildFile.Open(workspacePath, directoryPackagesPropsPath).GetDependencies(); + return dependencies.Select(d => + { + if (d.Version == null) + { + return d; + } + + var evaluation = MSBuildHelper.GetEvaluatedValue(d.Version, properties); + return d with + { + Version = evaluation.EvaluatedValue, + EvaluationResult = evaluation, + IsDirect = true, + }; + }); + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs index c93a0234431..ee007b367df 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs @@ -5,6 +5,7 @@ namespace NuGetUpdater.Core.Discover; public sealed record DirectoryPackagesPropsDiscoveryResult : IDiscoveryResultWithDependencies { public required string FilePath { get; init; } + public bool IsSuccess { get; init; } = true; public bool IsTransitivePinningEnabled { get; init; } public ImmutableArray Dependencies { get; init; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index f49577889e1..2b855d595cd 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -2,6 +2,9 @@ using System.Text.Json; using System.Text.Json.Serialization; +using NuGetUpdater.Core.Utilities; + + namespace NuGetUpdater.Core.Discover; public partial class DiscoveryWorker @@ -9,7 +12,7 @@ public partial class DiscoveryWorker public const string DiscoveryResultFileName = "./.dependabot/discovery.json"; private readonly Logger _logger; - private readonly HashSet _processedProjectPaths = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _processedProjectPaths = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _restoredMSBuildSdks = new(StringComparer.OrdinalIgnoreCase); internal static readonly JsonSerializerOptions SerializerOptions = new() { @@ -31,6 +34,10 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out { workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath)); } + else if (workspacePath == "/") + { + workspacePath = repoRootPath; + } DotNetToolsJsonDiscoveryResult? dotNetToolsJsonDiscovery = null; GlobalJsonDiscoveryResult? globalJsonDiscovery = null; @@ -40,13 +47,24 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out if (Directory.Exists(workspacePath)) { + _logger.Log($"Discovering build files in workspace [{workspacePath}]."); + dotNetToolsJsonDiscovery = DotNetToolsJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); globalJsonDiscovery = GlobalJsonDiscovery.Discover(repoRootPath, workspacePath, _logger); - ImmutableArray externalProperties = []; - projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath, externalProperties); + if (globalJsonDiscovery is not null) + { + await TryRestoreMSBuildSdksAsync(repoRootPath, workspacePath, globalJsonDiscovery.Dependencies, _logger); + } + + projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath); directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger); + + if (directoryPackagesPropsDiscovery is not null) + { + projectResults = projectResults.Remove(projectResults.First(p => p.FilePath.Equals(directoryPackagesPropsDiscovery.FilePath, StringComparison.OrdinalIgnoreCase))); + } } else { @@ -59,7 +77,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out DotNetToolsJson = dotNetToolsJsonDiscovery, GlobalJson = globalJsonDiscovery, DirectoryPackagesProps = directoryPackagesPropsDiscovery, - Projects = projectResults, + Projects = projectResults.OrderBy(p => p.FilePath).ToImmutableArray(), }; await WriteResults(repoRootPath, outputPath, result); @@ -69,31 +87,57 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out _processedProjectPaths.Clear(); } - private async Task> RunForDirectoryAsnyc(string repoRootPath, string workspacePath, ImmutableArray externalProperties) + /// + /// Restores MSBuild SDKs from the given dependencies. + /// + /// Returns `true` when SDKs were restored successfully. + private async Task TryRestoreMSBuildSdksAsync(string repoRootPath, string workspacePath, ImmutableArray dependencies, Logger logger) + { + var msbuildSdks = dependencies + .Where(d => d.Type == DependencyType.MSBuildSdk && !string.IsNullOrEmpty(d.Version)) + .Where(d => !d.Name.Equals("Microsoft.NET.Sdk", StringComparison.OrdinalIgnoreCase)) + .Where(d => !_restoredMSBuildSdks.Contains($"{d.Name}/{d.Version}")) + .ToImmutableArray(); + + if (msbuildSdks.Length == 0) + { + return false; + } + + var keys = msbuildSdks.Select(d => $"{d.Name}/{d.Version}"); + + _restoredMSBuildSdks.AddRange(keys); + + _logger.Log($" Restoring MSBuild SDKs: {string.Join(", ", keys)}"); + + return await NuGetHelper.DownloadNuGetPackagesAsync(repoRootPath, workspacePath, msbuildSdks, logger); + } + + private async Task> RunForDirectoryAsnyc(string repoRootPath, string workspacePath) { - _logger.Log($"Running for directory [{Path.GetRelativePath(repoRootPath, workspacePath)}]"); + _logger.Log($" Discovering projects beneath [{Path.GetRelativePath(repoRootPath, workspacePath)}]."); var projectPaths = FindProjectFiles(workspacePath); if (projectPaths.IsEmpty) { - _logger.Log("No project files found."); + _logger.Log(" No project files found."); return []; } - return await RunForProjectPathsAsync(repoRootPath, workspacePath, projectPaths, externalProperties); + return await RunForProjectPathsAsync(repoRootPath, workspacePath, projectPaths); } private static ImmutableArray FindProjectFiles(string workspacePath) { - return Directory.EnumerateFiles(workspacePath, "*.??proj", SearchOption.AllDirectories) + return Directory.EnumerateFiles(workspacePath, "*.*proj", SearchOption.AllDirectories) .Where(path => { var extension = Path.GetExtension(path).ToLowerInvariant(); - return extension == ".csproj" || extension == ".fsproj" || extension == ".vbproj"; + return extension == ".proj" || extension == ".csproj" || extension == ".fsproj" || extension == ".vbproj"; }) .ToImmutableArray(); } - private async Task> RunForProjectPathsAsync(string repoRootPath, string workspacePath, IEnumerable projectPaths, ImmutableArray externalProperties) + private async Task> RunForProjectPathsAsync(string repoRootPath, string workspacePath, IEnumerable projectPaths) { var results = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var projectPath in projectPaths) @@ -114,7 +158,19 @@ private async Task> RunForProjectPathsAsy var packagesConfigDependencies = PackagesConfigDiscovery.Discover(workspacePath, projectPath, _logger) ?.Dependencies; - var projectResults = await SdkProjectDiscovery.DiscoverAsync(repoRootPath, workspacePath, projectPath, externalProperties, _logger); + var projectResults = await SdkProjectDiscovery.DiscoverAsync(repoRootPath, workspacePath, projectPath, _logger); + + // Determine if there were unrestored MSBuildSdks + var msbuildSdks = projectResults.SelectMany(p => p.Dependencies.Where(d => d.Type == DependencyType.MSBuildSdk)).ToImmutableArray(); + if (msbuildSdks.Length > 0) + { + // If new SDKs were restored, then we need to rerun SdkProjectDiscovery. + if (await TryRestoreMSBuildSdksAsync(repoRootPath, workspacePath, msbuildSdks, _logger)) + { + projectResults = await SdkProjectDiscovery.DiscoverAsync(repoRootPath, workspacePath, projectPath, _logger); + } + } + foreach (var projectResult in projectResults) { if (results.ContainsKey(projectResult.FilePath)) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs index 72e58bf0f86..8bac2310aea 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs @@ -16,12 +16,15 @@ internal static class DotNetToolsJsonDiscovery logger.Log($" Discovered [{dotnetToolsJsonFile.RelativePath}] file."); - var dependencies = BuildFile.GetDependencies(dotnetToolsJsonFile); + var dependencies = BuildFile.GetDependencies(dotnetToolsJsonFile) + .OrderBy(d => d.Name) + .ToImmutableArray(); return new() { FilePath = dotnetToolsJsonFile.RelativePath, - Dependencies = dependencies.ToImmutableArray(), + IsSuccess = !dotnetToolsJsonFile.FailedToParse, + Dependencies = dependencies, }; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs index e3247ee8207..b2068afeee5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs @@ -5,5 +5,6 @@ namespace NuGetUpdater.Core.Discover; public sealed record DotNetToolsJsonDiscoveryResult : IDiscoveryResultWithDependencies { public required string FilePath { get; init; } + public bool IsSuccess { get; init; } = true; public ImmutableArray Dependencies { get; init; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs index b675b57db8e..3f23ab826b6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs @@ -16,12 +16,15 @@ internal static class GlobalJsonDiscovery logger.Log($" Discovered [{globalJsonFile.RelativePath}] file."); - var dependencies = BuildFile.GetDependencies(globalJsonFile); + var dependencies = BuildFile.GetDependencies(globalJsonFile) + .OrderBy(d => d.Name) + .ToImmutableArray(); return new() { FilePath = globalJsonFile.RelativePath, - Dependencies = dependencies.ToImmutableArray(), + IsSuccess = !globalJsonFile.FailedToParse, + Dependencies = dependencies, }; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs index 186c4133a9b..e71c7b80c93 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs @@ -5,5 +5,6 @@ namespace NuGetUpdater.Core.Discover; public sealed record GlobalJsonDiscoveryResult : IDiscoveryResultWithDependencies { public required string FilePath { get; init; } + public bool IsSuccess { get; init; } = true; public ImmutableArray Dependencies { get; init; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs index 45f58eb659d..cd59b8550eb 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs @@ -5,6 +5,7 @@ namespace NuGetUpdater.Core.Discover; public interface IDiscoveryResult { string FilePath { get; } + bool IsSuccess { get; } } public interface IDiscoveryResultWithDependencies : IDiscoveryResult diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs index 4e235d155f6..d355f52495f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs @@ -16,12 +16,14 @@ internal static class PackagesConfigDiscovery logger.Log($" Discovered [{packagesConfigFile.RelativePath}] file."); - var dependencies = BuildFile.GetDependencies(packagesConfigFile); + var dependencies = BuildFile.GetDependencies(packagesConfigFile) + .OrderBy(d => d.Name) + .ToImmutableArray(); return new() { FilePath = packagesConfigFile.RelativePath, - Dependencies = dependencies.ToImmutableArray(), + Dependencies = dependencies, }; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs index 1f278f3f97e..8953d9e591f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs @@ -5,5 +5,6 @@ namespace NuGetUpdater.Core.Discover; public sealed record PackagesConfigDiscoveryResult : IDiscoveryResultWithDependencies { public required string FilePath { get; init; } + public bool IsSuccess { get; init; } = true; public ImmutableArray Dependencies { get; init; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs index 15edb4b45bd..c4399aa08aa 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs @@ -5,8 +5,9 @@ namespace NuGetUpdater.Core.Discover; public record ProjectDiscoveryResult : IDiscoveryResultWithDependencies { public required string FilePath { get; init; } - public required ImmutableDictionary Properties { get; init; } + public required ImmutableArray Dependencies { get; init; } + public bool IsSuccess { get; init; } = true; + public ImmutableArray Properties { get; init; } = []; public ImmutableArray TargetFrameworks { get; init; } = []; public ImmutableArray ReferencedProjectPaths { get; init; } = []; - public required ImmutableArray Dependencies { get; init; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs index 2b4b5bc8281..e495a8bdc2b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs @@ -6,22 +6,31 @@ namespace NuGetUpdater.Core.Discover; internal static class SdkProjectDiscovery { - public static async Task> DiscoverAsync(string repoRootPath, string workspacePath, string projectPath, ImmutableArray externalProperties, Logger logger) + public static async Task> DiscoverAsync(string repoRootPath, string workspacePath, string projectPath, Logger logger) { // Determine which targets and props files contribute to the build. - var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath); + var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath, includeSdkPropsAndTargets: true); // Get all the dependencies which are directly referenced from the project file or indirectly referenced from // targets and props files. - var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles, externalProperties); + var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles); var results = ImmutableArray.CreateBuilder(); foreach (var buildFile in buildFiles) { + // Only include build files that exist beneath the RepoRootPath. + if (buildFile.IsOutsideBasePath) + { + continue; + } + // The build file dependencies have the correct DependencyType and the TopLevelDependencies have the evaluated version. // Combine them to have the set of dependencies that are directly referenced from the build file. var fileDependencies = BuildFile.GetDependencies(buildFile) .ToDictionary(d => d.Name, StringComparer.OrdinalIgnoreCase); + var sdkDependencies = fileDependencies.Values + .Where(d => d.Type == DependencyType.MSBuildSdk) + .ToImmutableArray(); var directDependencies = topLevelDependencies .Where(d => fileDependencies.ContainsKey(d.Name)) .Select(d => @@ -37,24 +46,33 @@ public static async Task> DiscoverAsync(s if (buildFile.GetFileType() == ProjectBuildFileType.Project) { // Collect information that is specific to the project file. - var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles, externalProperties).ToImmutableArray(); - var properties = MSBuildHelper.GetProperties(buildFiles, externalProperties).ToImmutableDictionary(); - var referencedProjectPaths = MSBuildHelper.GetProjectPathsFromProject(projectPath).ToImmutableArray(); + var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles) + .OrderBy(tfm => tfm) + .ToImmutableArray(); + var properties = MSBuildHelper.GetProperties(buildFiles).Values + .Where(p => !p.SourceFilePath.StartsWith("..")) + .OrderBy(p => p.Name) + .ToImmutableArray(); + var referencedProjectPaths = MSBuildHelper.GetProjectPathsFromProject(projectPath) + .Select(path => Path.GetRelativePath(workspacePath, path)) + .OrderBy(p => p) + .ToImmutableArray(); // Get the complete set of dependencies including transitive dependencies. directDependencies = directDependencies .Select(d => d with { TargetFrameworks = tfms }) .ToImmutableArray(); var transitiveDependencies = await GetTransitiveDependencies(repoRootPath, projectPath, tfms, directDependencies, logger); - ImmutableArray dependencies = [.. directDependencies, .. transitiveDependencies]; + ImmutableArray dependencies = directDependencies.Concat(transitiveDependencies).Concat(sdkDependencies) + .OrderBy(d => d.Name) + .ToImmutableArray(); results.Add(new() { FilePath = Path.GetRelativePath(workspacePath, buildFile.Path), Properties = properties, TargetFrameworks = tfms, - ReferencedProjectPaths = referencedProjectPaths - .Select(path => Path.GetRelativePath(workspacePath, path)).ToImmutableArray(), + ReferencedProjectPaths = referencedProjectPaths, Dependencies = dependencies, }); } @@ -63,8 +81,9 @@ public static async Task> DiscoverAsync(s results.Add(new() { FilePath = Path.GetRelativePath(workspacePath, buildFile.Path), - Properties = ImmutableDictionary.Empty, - Dependencies = directDependencies, + Dependencies = directDependencies.Concat(sdkDependencies) + .OrderBy(d => d.Name) + .ToImmutableArray(), }); } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs index 52c36627227..cae5259bb4c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs @@ -5,6 +5,7 @@ namespace NuGetUpdater.Core.Discover; public sealed record WorkspaceDiscoveryResult : IDiscoveryResult { public required string FilePath { get; init; } + public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } public GlobalJsonDiscoveryResult? GlobalJson { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs index 99d6061f54c..cf932443aa5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs @@ -4,6 +4,5 @@ public record EvaluationResult( EvaluationResultType ResultType, string OriginalValue, string EvaluatedValue, - string? FirstPropertyName, - string? LastPropertyName, + string? RootPropertyName, string? ErrorMessage); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs index 82572673c0b..ca5007adb7b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs @@ -11,6 +11,8 @@ internal abstract class BuildFile public string BasePath { get; } public string Path { get; } public string RelativePath => System.IO.Path.GetRelativePath(BasePath, Path); + public bool IsOutsideBasePath => RelativePath.StartsWith(".."); + public bool FailedToParse { get; protected set; } public BuildFile(string basePath, string path) { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs index b21595787d7..a93233102b6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs @@ -16,6 +16,28 @@ public GlobalJsonBuildFile(string basePath, string path, string contents, Logger public JsonObject? MSBuildSdks => Node.Value is JsonObject root ? root["msbuild-sdks"]?.AsObject() : null; - public IEnumerable GetDependencies() => MSBuildSdks?.AsObject().Select( - t => new Dependency(t.Key, t.Value?.GetValue() ?? string.Empty, DependencyType.MSBuildSdk)) ?? Enumerable.Empty(); + public IEnumerable GetDependencies() + { + List dependencies = []; + if (Sdk is not null + && Sdk.TryGetPropertyValue("version", out var version)) + { + dependencies.Add(GetSdkDependency("Microsoft.NET.Sdk", version)); + } + + if (MSBuildSdks is null) + { + return dependencies; + } + + var msBuildDependencies = MSBuildSdks + .Select(t => GetSdkDependency(t.Key, t.Value)); + dependencies.AddRange(msBuildDependencies); + return dependencies; + } + + private Dependency GetSdkDependency(string name, JsonNode? version) + { + return new Dependency(name, version?.GetValue(), DependencyType.MSBuildSdk); + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs index d7790d79910..2a83f55f5f3 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs @@ -40,6 +40,7 @@ private void ResetNode() // We can't police that people have legal JSON files. // If they don't, we just return null. logger.Log($"Failed to parse JSON file: {RelativePath}, got {ex}"); + FailedToParse = true; return null; } }); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs index 25c15252df9..dadca2d58b4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs @@ -22,6 +22,6 @@ public IEnumerable GetDependencies() => Packages .Select(p => new Dependency( p.GetAttributeValue("id", StringComparison.OrdinalIgnoreCase), p.GetAttributeValue("version", StringComparison.OrdinalIgnoreCase), - DependencyType.PackageConfig, + DependencyType.PackagesConfig, IsDevDependency: (p.GetAttribute("developmentDependency", StringComparison.OrdinalIgnoreCase)?.Value ?? "false").Equals(true.ToString(), StringComparison.OrdinalIgnoreCase))); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs index 1bf59650084..ffbba0e711c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs @@ -15,14 +15,22 @@ public ProjectBuildFile(string basePath, string path, XmlDocumentSyntax contents { } - public IEnumerable PropertyNodes => Contents.RootSyntax + public IXmlElementSyntax ProjectNode => Contents.RootSyntax; + + public IEnumerable SdkNodes => ProjectNode + .GetElements("Sdk", StringComparison.OrdinalIgnoreCase); + + public IEnumerable ImportNodes => ProjectNode + .GetElements("Import", StringComparison.OrdinalIgnoreCase); + + public IEnumerable PropertyNodes => ProjectNode .GetElements("PropertyGroup", StringComparison.OrdinalIgnoreCase) .SelectMany(e => e.Elements); public IEnumerable> GetProperties() => PropertyNodes .Select(e => new KeyValuePair(e.Name, e.GetContentValue())); - public IEnumerable ItemNodes => Contents.RootSyntax + public IEnumerable ItemNodes => ProjectNode .GetElements("ItemGroup", StringComparison.OrdinalIgnoreCase) .SelectMany(e => e.Elements); @@ -31,14 +39,67 @@ public IEnumerable> GetProperties() => PropertyNode e.Name.Equals("GlobalPackageReference", StringComparison.OrdinalIgnoreCase) || e.Name.Equals("PackageVersion", StringComparison.OrdinalIgnoreCase)); - public IEnumerable GetDependencies() => PackageItemNodes - .Select(GetDependency) - .OfType(); + public IEnumerable GetDependencies() + { + var sdkDependencies = GetSdkDependencies(); + var packageDependencies = PackageItemNodes + .Select(GetPackageDependency) + .OfType(); + return sdkDependencies.Concat(packageDependencies); + } + + private IEnumerable GetSdkDependencies() + { + List dependencies = []; + if (ProjectNode.GetAttributeValueCaseInsensitive("Sdk") is string sdk) + { + dependencies.Add(GetMSBuildSdkDependency(sdk)); + } + + foreach (var sdkNode in SdkNodes) + { + var name = sdkNode.GetAttributeValueCaseInsensitive("Name"); + var version = sdkNode.GetAttributeValueCaseInsensitive("Version"); + + if (name is not null) + { + dependencies.Add(GetMSBuildSdkDependency(name, version)); + } + } + + foreach (var importNode in ImportNodes) + { + var name = importNode.GetAttributeValueCaseInsensitive("Name"); + var version = importNode.GetAttributeValueCaseInsensitive("Version"); + + if (name is not null) + { + dependencies.Add(GetMSBuildSdkDependency(name, version)); + } + } - private static Dependency? GetDependency(IXmlElementSyntax element) + return dependencies; + } + + private static Dependency GetMSBuildSdkDependency(string name, string? version = null) { - var name = element.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase) - ?? element.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase); + var parts = name.Split('/'); + return parts.Length == 2 + ? new Dependency(parts[0], parts[1], DependencyType.MSBuildSdk) + : new Dependency(name, version, DependencyType.MSBuildSdk); + } + + private static Dependency? GetPackageDependency(IXmlElementSyntax element) + { + var isUpdate = false; + + var name = element.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase); + if (name is null) + { + isUpdate = true; + name = element.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase); + } + if (name is null || name.StartsWith("@(")) { return null; @@ -56,6 +117,7 @@ public IEnumerable GetDependencies() => PackageItemNodes Name: name, Version: version?.Length == 0 ? null : version, Type: GetDependencyType(element.Name), + IsUpdate: isUpdate, IsOverride: isVersionOverride); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 858ec4d913a..a5a33bcc60f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Language.Xml; @@ -26,10 +21,10 @@ public static async Task UpdateDependencyAsync( logger.Log(" Running for SDK-style project"); var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath); - var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles, externalProperties: []); + var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles); // Get the set of all top-level dependencies in the current project - var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles, externalProperties: []).ToArray(); + var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray(); if (!await DoesDependencyRequireUpdateAsync(repoRootPath, projectPath, tfms, topLevelDependencies, dependencyName, newDependencyVersion, logger)) { return; @@ -564,7 +559,7 @@ private static IEnumerable FindPackageNodes( private static async Task AreDependenciesCoherentAsync(string repoRootPath, string projectPath, string dependencyName, Logger logger, ImmutableArray buildFiles, string[] tfms) { - var updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles, []).ToArray(); + var updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray(); foreach (var tfm in tfms) { var updatedPackages = await MSBuildHelper.GetAllPackageDependenciesAsync(repoRootPath, projectPath, tfm, updatedTopLevelDependencies, logger); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs new file mode 100644 index 00000000000..41e8051a030 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Utilities; + +public static class HashSetExtensions +{ + public static void AddRange(this HashSet hashSet, IEnumerable items) + { + foreach (var item in items) + { + hashSet.Add(item); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index cdb099f73ec..202b1cb0013 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -40,16 +40,11 @@ public static void RegisterMSBuild() } } - public static string[] GetTargetFrameworkMonikers(ImmutableArray buildFiles, ImmutableArray externalProperties) + public static string[] GetTargetFrameworkMonikers(ImmutableArray buildFiles) { HashSet targetFrameworkValues = new(StringComparer.OrdinalIgnoreCase); Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); - foreach (var property in externalProperties) - { - propertyInfo[property.Name] = property; - } - foreach (var buildFile in buildFiles) { var projectRoot = CreateProjectRootElement(buildFile); @@ -59,6 +54,11 @@ public static string[] GetTargetFrameworkMonikers(ImmutableArray GetProjectPathsFromProject(string projFilePath } } - public static IReadOnlyDictionary GetProperties(ImmutableArray buildFiles, ImmutableArray externalProperties) + public static IReadOnlyDictionary GetProperties(ImmutableArray buildFiles) { Dictionary properties = new(StringComparer.OrdinalIgnoreCase); - foreach (var property in externalProperties) - { - properties[property.Name] = property; - } - foreach (var buildFile in buildFiles) { var projectRoot = CreateProjectRootElement(buildFile); @@ -199,21 +199,35 @@ public static IReadOnlyDictionary GetProperties(ImmutableArray return properties; } - public static IEnumerable GetTopLevelPackageDependencyInfos(ImmutableArray buildFiles, ImmutableArray externalProperties) + public static IEnumerable GetTopLevelPackageDependencyInfos(ImmutableArray buildFiles) { Dictionary packageInfo = new(StringComparer.OrdinalIgnoreCase); Dictionary packageVersionInfo = new(StringComparer.OrdinalIgnoreCase); Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); - foreach (var property in externalProperties) - { - propertyInfo[property.Name] = property; - } - foreach (var buildFile in buildFiles) { var projectRoot = CreateProjectRootElement(buildFile); + foreach (var property in projectRoot.Properties) + { + // Short of evaluating the entire project, there's no way to _really_ know what package version is + // going to be used, and even then we might not be able to update it. As a best guess, we'll simply + // skip any property that has a condition _or_ where the condition is checking for an empty string. + var hasEmptyCondition = string.IsNullOrEmpty(property.Condition); + var conditionIsCheckingForEmptyString = string.Equals(property.Condition, $"$({property.Name}) == ''", StringComparison.OrdinalIgnoreCase) || + string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase); + if (hasEmptyCondition || conditionIsCheckingForEmptyString) + { + propertyInfo[property.Name] = new(property.Name, property.Value, buildFile.RelativePath); + } + } + + if (buildFile.RelativePath.StartsWith("..")) + { + continue; + } + foreach (var packageItem in projectRoot.Items .Where(i => (i.ItemType == "PackageReference" || i.ItemType == "GlobalPackageReference"))) { @@ -250,20 +264,6 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl packageVersionInfo[packageItem.Include] = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value ?? string.Empty; } - - foreach (var property in projectRoot.Properties) - { - // Short of evaluating the entire project, there's no way to _really_ know what package version is - // going to be used, and even then we might not be able to update it. As a best guess, we'll simply - // skip any property that has a condition _or_ where the condition is checking for an empty string. - var hasEmptyCondition = string.IsNullOrEmpty(property.Condition); - var conditionIsCheckingForEmptyString = string.Equals(property.Condition, $"$({property.Name}) == ''", StringComparison.OrdinalIgnoreCase) || - string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase); - if (hasEmptyCondition || conditionIsCheckingForEmptyString) - { - propertyInfo[property.Name] = new(property.Name, property.Value, buildFile.RelativePath); - } - } } foreach (var (name, info) in packageInfo) @@ -276,12 +276,9 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl // Walk the property replacements until we don't find another one. var evaluationResult = GetEvaluatedValue(packageVersion, propertyInfo); - if (evaluationResult.ResultType != EvaluationResultType.Success) - { - throw new InvalidDataException(evaluationResult.ErrorMessage); - } - - packageVersion = evaluationResult.EvaluatedValue.TrimStart('[', '(').TrimEnd(']', ')'); + packageVersion = evaluationResult.ResultType == EvaluationResultType.Success + ? evaluationResult.EvaluatedValue.TrimStart('[', '(').TrimEnd(']', ')') + : evaluationResult.EvaluatedValue; // We don't know the version for range requirements or wildcard // requirements, so return "" for these. @@ -300,32 +297,30 @@ public static EvaluationResult GetEvaluatedValue(string msbuildString, IReadOnly var seenProperties = new HashSet(StringComparer.OrdinalIgnoreCase); string originalValue = msbuildString; - string? firstPropertyName = null; - string? lastPropertyName = null; + string? rootPropertyName = null; while (TryGetPropertyName(msbuildString, out var propertyName)) { - firstPropertyName ??= propertyName; - lastPropertyName = propertyName; + rootPropertyName = propertyName; if (ignoredProperties.Contains(propertyName)) { - return new(EvaluationResultType.PropertyIgnored, originalValue, msbuildString, firstPropertyName, lastPropertyName, $"Property '{propertyName}' is ignored."); + return new(EvaluationResultType.PropertyIgnored, originalValue, msbuildString, rootPropertyName, $"Property '{propertyName}' is ignored."); } if (!seenProperties.Add(propertyName)) { - return new(EvaluationResultType.CircularReference, originalValue, msbuildString, firstPropertyName, lastPropertyName, $"Property '{propertyName}' has a circular reference."); + return new(EvaluationResultType.CircularReference, originalValue, msbuildString, rootPropertyName, $"Property '{propertyName}' has a circular reference."); } if (!propertyInfo.TryGetValue(propertyName, out var property)) { - return new(EvaluationResultType.PropertyNotFound, originalValue, msbuildString, firstPropertyName, lastPropertyName, $"Property '{propertyName}' was not found."); + return new(EvaluationResultType.PropertyNotFound, originalValue, msbuildString, rootPropertyName, $"Property '{propertyName}' was not found."); } msbuildString = msbuildString.Replace($"$({propertyName})", property.Value); } - return new(EvaluationResultType.Success, originalValue, msbuildString, firstPropertyName, lastPropertyName, null); + return new(EvaluationResultType.Success, originalValue, msbuildString, rootPropertyName, null); } public static bool TryGetPropertyName(string versionContent, [NotNullWhen(true)] out string? propertyName) @@ -392,12 +387,13 @@ private static ProjectRootElement CreateProjectRootElement(ProjectBuildFile buil } } - private static async Task CreateTempProjectAsync( + internal static async Task CreateTempProjectAsync( DirectoryInfo tempDir, string repoRoot, string projectPath, string targetFramework, - IReadOnlyCollection packages) + IReadOnlyCollection packages, + bool usePackageDownload = false) { var projectDirectory = Path.GetDirectoryName(projectPath); projectDirectory ??= repoRoot; @@ -431,7 +427,7 @@ private static async Task CreateTempProjectAsync( // empty `Version` attributes will cause the temporary project to not build .Where(p => !string.IsNullOrWhiteSpace(p.Version)) // If all PackageReferences for a package are update-only mark it as such, otherwise it can cause package incoherence errors which do not exist in the repo. - .Select(static p => $"")); + .Select(p => $"<{(usePackageDownload ? "PackageDownload" : "PackageReference")} {(p.IsUpdate ? "Update" : "Include")}=\"{p.Name}\" Version=\"[{p.Version}]\" />")); var projectContents = $""" @@ -536,23 +532,23 @@ internal static async Task GetAllPackageDependenciesAsync( internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath) { - globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json"); + globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json", caseSensitive: false); return globalJsonPath is not null; } internal static bool TryGetDotNetToolsJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? dotnetToolsJsonJsonPath) { - dotnetToolsJsonJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./.config/dotnet-tools.json"); + dotnetToolsJsonJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./.config/dotnet-tools.json", caseSensitive: false); return dotnetToolsJsonJsonPath is not null; } internal static bool TryGetDirectoryPackagesPropsPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? directoryPackagesPropsPath) { - directoryPackagesPropsPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./Directory.Packages.props"); + directoryPackagesPropsPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./Directory.Packages.props", caseSensitive: false); return directoryPackagesPropsPath is not null; } - internal static async Task> LoadBuildFilesAsync(string repoRootPath, string projectPath) + internal static async Task> LoadBuildFilesAsync(string repoRootPath, string projectPath, bool includeSdkPropsAndTargets = false) { var buildFileList = new List { @@ -610,11 +606,12 @@ internal static async Task> LoadBuildFilesAsync } var repoRootPathPrefix = repoRootPath.NormalizePathToUnix() + "/"; - var buildFilesInRepo = buildFileList - .Where(f => f.StartsWith(repoRootPathPrefix, StringComparison.OrdinalIgnoreCase)) - .Distinct() - .ToArray(); - var result = buildFilesInRepo + var buildFiles = includeSdkPropsAndTargets + ? buildFileList.Distinct() + : buildFileList + .Where(f => f.StartsWith(repoRootPathPrefix, StringComparison.OrdinalIgnoreCase)) + .Distinct(); + var result = buildFiles .Select(path => ProjectBuildFile.Open(repoRootPath, path)) .ToImmutableArray(); return result; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs index e971d0f6f05..dd5087f24c5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs @@ -19,4 +19,20 @@ public static bool TryGetPackagesConfigFile(string projectPath, [NotNullWhen(ret packagesConfigPath = null; return false; } + + internal static async Task DownloadNuGetPackagesAsync(string repoRoot, string projectPath, IReadOnlyCollection packages, Logger logger) + { + var tempDirectory = Directory.CreateTempSubdirectory("msbuild_sdk_restore_"); + try + { + var tempProjectPath = await MSBuildHelper.CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, "netstandard2.0", packages, usePackageDownload: true); + var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\""); + + return exitCode == 0; + } + finally + { + tempDirectory.Delete(recursive: true); + } + } } From d9c904a114499a549e0d26533cd2cedebf94256f Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sat, 23 Mar 2024 00:54:22 -0700 Subject: [PATCH 16/26] Updated ruby side. Made progress on tests. --- .../nuget/discovery/dependency_details.rb | 6 +- .../discovery/dependency_file_discovery.rb | 46 +- .../nuget/discovery/discovery_json_reader.rb | 5 +- .../nuget/discovery/evaluation_details.rb | 20 +- .../nuget/discovery/project_discovery.rb | 2 +- nuget/lib/dependabot/nuget/file_parser.rb | 1 + .../nuget/update_checker/tfm_finder.rb | 5 +- .../dotnet_tools_json_parser_spec.rb | 74 -- .../file_parser/global_json_parser_spec.rb | 56 - .../packages_config_parser_spec.rb | 65 - .../file_parser/project_file_parser_spec.rb | 1118 ----------------- .../file_parser/property_value_finder_spec.rb | 206 --- .../spec/dependabot/nuget/file_parser_spec.rb | 345 ++--- .../nuget/update_checker/tfm_finder_spec.rb | 57 +- nuget/spec/fixtures/csproj/basic.csproj | 4 +- .../projects/file_parser_csproj/my.csproj | 22 + .../Directory.Packages.props | 11 + .../my.csproj | 22 + .../commonprops.props | 5 + .../my.csproj | 24 + .../Directory.Build.targets | 3 + .../Packages.props | 10 + .../my.csproj | 22 + .../file_parser_csproj_unparsable/my.csproj | 9 + .../file_parser_csproj_vbproj/my.csproj | 22 + .../file_parser_csproj_vbproj/my.vbproj | 6 + .../file_parser_packages_config/empty.csproj | 2 + .../packages.config | 13 + .../.config/dotnet-tools.json | 18 + .../empty.csproj | 2 + .../packages.config | 13 + .../empty.csproj | 2 + .../global.json | 8 + .../packages.config | 13 + .../dir/empty.csproj | 2 + .../dir/packages.config | 13 + .../projects/file_parser_proj/proj.proj | 6 + .../fixtures/projects/tfm_finder/my.csproj | 10 + .../projects/tfm_finder/ref/another.csproj | 9 + 39 files changed, 427 insertions(+), 1850 deletions(-) delete mode 100644 nuget/spec/dependabot/nuget/file_parser/dotnet_tools_json_parser_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/file_parser/global_json_parser_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/file_parser/packages_config_parser_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/file_parser/property_value_finder_spec.rb create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj/my.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_directory_packages_props/Directory.Packages.props create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_directory_packages_props/my.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_imported_props/commonprops.props create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_imported_props/my.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_packages_props/Directory.Build.targets create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_packages_props/Packages.props create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_packages_props/my.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_unparsable/my.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_vbproj/my.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_vbproj/my.vbproj create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config/empty.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config/packages.config create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/.config/dotnet-tools.json create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/empty.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/packages.config create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config_global_json/empty.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config_global_json/global.json create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config_global_json/packages.config create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config_nested/dir/empty.csproj create mode 100644 nuget/spec/fixtures/projects/file_parser_packages_config_nested/dir/packages.config create mode 100644 nuget/spec/fixtures/projects/file_parser_proj/proj.proj create mode 100644 nuget/spec/fixtures/projects/tfm_finder/my.csproj create mode 100644 nuget/spec/fixtures/projects/tfm_finder/ref/another.csproj diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_details.rb b/nuget/lib/dependabot/nuget/discovery/dependency_details.rb index 6a72f4aa3f2..c40f1922a8c 100644 --- a/nuget/lib/dependabot/nuget/discovery/dependency_details.rb +++ b/nuget/lib/dependabot/nuget/discovery/dependency_details.rb @@ -12,7 +12,7 @@ class DependencyDetails sig { params(json: T::Hash[String, T.untyped]).returns(DependencyDetails) } def self.from_json(json) name = T.let(json.fetch("Name"), String) - version = T.let(json.fetch("Version"), String) + version = T.let(json.fetch("Version"), T.nilable(String)) type = T.let(json.fetch("Type"), String) evaluation = EvaluationDetails .from_json(T.let(json.fetch("EvaluationResult"), T.nilable(T::Hash[String, T.untyped]))) @@ -35,7 +35,7 @@ def self.from_json(json) sig do params(name: String, - version: String, + version: T.nilable(String), type: String, evaluation: T.nilable(EvaluationDetails), is_dev_dependency: T::Boolean, @@ -60,7 +60,7 @@ def initialize(name:, version:, type:, evaluation:, is_dev_dependency:, is_direc sig { returns(String) } attr_reader :name - sig { returns(String) } + sig { returns(T.nilable(String)) } attr_reader :version sig { returns(String) } diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb index b6798a2f6be..e12df77a149 100644 --- a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb @@ -38,24 +38,35 @@ def initialize(file_path:, dependencies:) attr_reader :dependencies sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def dependency_set + def dependency_set # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/AbcSize dependency_set = Dependabot::FileParsers::Base::DependencySet.new file_name = Pathname.new(file_path).cleanpath.to_path - dependencies.each do |dependency_details| + dependencies.each do |dependency| + next if dependency.name.casecmp("Microsoft.NET.Sdk")&.zero? + + # If the version string was evaluated it must have been successfully resolved + if dependency.evaluation && dependency.evaluation&.result_type != "Success" + logger.warn "Dependency '#{dependency.name}' excluded due to unparsable version: #{dependency.version}" + next + end + # Exclude any dependencies using version ranges or wildcards - next if dependency_details.version == "" || - dependency_details.version.include?(",") || - dependency_details.version.include?("*") + next if dependency.version&.include?(",") || + dependency.version&.include?("*") # Exclude any dependencies specified using interpolation - next if dependency_details.name.include?("%(") || - dependency_details.version.include?("%(") + next if dependency.name.include?("%(") || + dependency.version&.include?("%(") - # Exclude any dependencies that are updates - next if dependency_details.is_update + dependency_file_name = file_name + if dependency.type == "PackagesConfig" + dir_name = File.dirname(file_name) + dependency_file_name = "packages.config" + dependency_file_name = File.join(dir_name, "packages.config") unless dir_name == "." + end - dependency_set << build_dependency(file_name, dependency_details) + dependency_set << build_dependency(dependency_file_name, dependency) end dependency_set @@ -63,12 +74,18 @@ def dependency_set private + sig { returns(::Logger) } + def logger + Dependabot.logger + end + sig { params(file_name: String, dependency_details: DependencyDetails).returns(Dependabot::Dependency) } def build_dependency(file_name, dependency_details) requirement = build_requirement(file_name, dependency_details) requirements = requirement.nil? ? [] : [requirement] - version = dependency_details.version.gsub(/[\(\)\[\]]/, "").strip + version = dependency_details.version&.gsub(/[\(\)\[\]]/, "")&.strip + version = nil if version&.empty? Dependency.new( name: dependency_details.name, @@ -85,14 +102,17 @@ def build_dependency(file_name, dependency_details) def build_requirement(file_name, dependency_details) return if dependency_details.is_transitive + version = dependency_details.version + version = nil if version&.empty? + requirement = { - requirement: dependency_details.version, + requirement: version, file: file_name, groups: [dependency_details.is_dev_dependency ? "devDependencies" : "dependencies"], source: nil } - property_name = dependency_details.evaluation&.last_property_name + property_name = dependency_details.evaluation&.root_property_name return requirement unless property_name requirement[:metadata] = { property_name: property_name } diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index 4e39cd63455..1577b39e683 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "dependabot/dependency" +require "dependabot/nuget/discovery/workspace_discovery" require "json" require "sorbet-runtime" @@ -36,7 +37,7 @@ def self.write_discovery_json(discovery_json) sig { returns(T.nilable(DependencyFile)) } def self.discovery_json - return @test_discovery_json if test_run? + # return @test_discovery_json if test_run? return unless File.exist?(discovery_file_path) @@ -78,6 +79,8 @@ def workspace_discovery @workspace_discovery ||= T.let(begin return nil unless discovery_json.content + puts discovery_json.content + parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) WorkspaceDiscovery.from_json(parsed_json) end, T.nilable(WorkspaceDiscovery)) diff --git a/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb b/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb index 2c1f0d50d98..028bd006991 100644 --- a/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb +++ b/nuget/lib/dependabot/nuget/discovery/evaluation_details.rb @@ -15,15 +15,13 @@ def self.from_json(json) result_type = T.let(json.fetch("ResultType"), String) original_value = T.let(json.fetch("OriginalValue"), String) evaluated_value = T.let(json.fetch("EvaluatedValue"), String) - first_property_name = T.let(json.fetch("FirstPropertyName", nil), T.nilable(String)) - last_property_name = T.let(json.fetch("LastPropertyName", nil), T.nilable(String)) + root_property_name = T.let(json.fetch("RootPropertyName", nil), T.nilable(String)) error_message = T.let(json.fetch("ErrorMessage", nil), T.nilable(String)) EvaluationDetails.new(result_type: result_type, original_value: original_value, evaluated_value: evaluated_value, - first_property_name: first_property_name, - last_property_name: last_property_name, + root_property_name: root_property_name, error_message: error_message) end @@ -31,21 +29,18 @@ def self.from_json(json) params(result_type: String, original_value: String, evaluated_value: String, - first_property_name: T.nilable(String), - last_property_name: T.nilable(String), + root_property_name: T.nilable(String), error_message: T.nilable(String)).void end def initialize(result_type:, original_value:, evaluated_value:, - first_property_name:, - last_property_name:, + root_property_name:, error_message:) @result_type = result_type @original_value = original_value @evaluated_value = evaluated_value - @first_property_name = first_property_name - @last_property_name = last_property_name + @root_property_name = root_property_name @error_message = error_message end @@ -59,10 +54,7 @@ def initialize(result_type:, attr_reader :evaluated_value sig { returns(T.nilable(String)) } - attr_reader :first_property_name - - sig { returns(T.nilable(String)) } - attr_reader :last_property_name + attr_reader :root_property_name sig { returns(T.nilable(String)) } attr_reader :error_message diff --git a/nuget/lib/dependabot/nuget/discovery/project_discovery.rb b/nuget/lib/dependabot/nuget/discovery/project_discovery.rb index fef9c73821e..fc669ed6bb3 100644 --- a/nuget/lib/dependabot/nuget/discovery/project_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/project_discovery.rb @@ -17,7 +17,7 @@ def self.from_json(json) return nil if json.nil? file_path = T.let(json.fetch("FilePath"), String) - properties = T.let(json.fetch("Properties"), T::Hash[String, T::Hash[String, T.untyped]]).values.map do |prop| + properties = T.let(json.fetch("Properties"), T::Array[T::Hash[String, T.untyped]]).map do |prop| PropertyDetails.from_json(prop) end target_frameworks = T.let(json.fetch("TargetFrameworks"), T::Array[String]) diff --git a/nuget/lib/dependabot/nuget/file_parser.rb b/nuget/lib/dependabot/nuget/file_parser.rb index 0390740ec89..fac909a40d1 100644 --- a/nuget/lib/dependabot/nuget/file_parser.rb +++ b/nuget/lib/dependabot/nuget/file_parser.rb @@ -5,6 +5,7 @@ require "dependabot/file_parsers" require "dependabot/file_parsers/base" require "dependabot/nuget/discovery/discovery_json_reader" +require "dependabot/nuget/native_helpers" require "sorbet-runtime" # For details on how dotnet handles version constraints, see: diff --git a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb b/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb index 04c5d074892..aadb0e83efd 100644 --- a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb @@ -19,7 +19,10 @@ def self.frameworks(dependency) return [] unless workspace workspace.projects.select do |project| - project.dependencies.any? { |d| d.name.casecmp?(dependency.name) } + all_dependencies = project.dependencies + project.referenced_project_paths.flat_map do |ref| + workspace.projects.find { |p| p.file_path == ref }&.dependencies || [] + end + all_dependencies.any? { |d| d.name.casecmp?(dependency.name) } end.flat_map(&:target_frameworks).uniq end end diff --git a/nuget/spec/dependabot/nuget/file_parser/dotnet_tools_json_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser/dotnet_tools_json_parser_spec.rb deleted file mode 100644 index 27b9d234dc9..00000000000 --- a/nuget/spec/dependabot/nuget/file_parser/dotnet_tools_json_parser_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency_file" -require "dependabot/source" -require "dependabot/nuget/file_parser/dotnet_tools_json_parser" - -RSpec.describe Dependabot::Nuget::FileParser::DotNetToolsJsonParser do - let(:file) do - Dependabot::DependencyFile.new(name: "dotnet-tools.json", content: file_body) - end - let(:file_body) { fixture("dotnet_tools_jsons", "dotnet-tools.json") } - let(:parser) { described_class.new(dotnet_tools_json: file) } - - describe "dependency_set" do - subject(:dependency_set) { parser.dependency_set } - - it { is_expected.to be_a(Dependabot::FileParsers::Base::DependencySet) } - - describe "the dependencies" do - subject(:dependencies) { dependency_set.dependencies } - - its(:length) { is_expected.to eq(2) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("botsay") - expect(dependency.version).to eq("1.0.0") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.0", - file: "dotnet-tools.json", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "the last dependency" do - subject(:dependency) { dependencies.last } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("dotnetsay") - expect(dependency.version).to eq("1.0.0") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.0", - file: "dotnet-tools.json", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - context "with bad JSON" do - let(:file_body) { fixture("dotnet_tools_jsons", "invalid_json.json") } - - it "raises a Dependabot::DependencyFileNotParseable error" do - expect { parser.dependency_set } - .to raise_error(Dependabot::DependencyFileNotParseable) do |error| - expect(error.file_name).to eq("dotnet-tools.json") - end - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/file_parser/global_json_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser/global_json_parser_spec.rb deleted file mode 100644 index 76660ce613b..00000000000 --- a/nuget/spec/dependabot/nuget/file_parser/global_json_parser_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency_file" -require "dependabot/source" -require "dependabot/nuget/file_parser/global_json_parser" - -RSpec.describe Dependabot::Nuget::FileParser::GlobalJsonParser do - let(:file) do - Dependabot::DependencyFile.new(name: "global.json", content: file_body) - end - let(:file_body) { fixture("global_jsons", "global.json") } - let(:parser) { described_class.new(global_json: file) } - - describe "dependency_set" do - subject(:dependency_set) { parser.dependency_set } - - it { is_expected.to be_a(Dependabot::FileParsers::Base::DependencySet) } - - describe "the dependencies" do - subject(:dependencies) { dependency_set.dependencies } - - its(:length) { is_expected.to eq(1) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Microsoft.Build.Traversal") - expect(dependency.version).to eq("1.0.45") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.45", - file: "global.json", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - context "with bad JSON" do - let(:file_body) { fixture("global_jsons", "invalid_json.json") } - - it "raises a Dependabot::DependencyFileNotParseable error" do - expect { parser.dependency_set } - .to raise_error(Dependabot::DependencyFileNotParseable) do |error| - expect(error.file_name).to eq("global.json") - end - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/file_parser/packages_config_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser/packages_config_parser_spec.rb deleted file mode 100644 index d3f790a9e2e..00000000000 --- a/nuget/spec/dependabot/nuget/file_parser/packages_config_parser_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency_file" -require "dependabot/source" -require "dependabot/nuget/file_parser/packages_config_parser" - -RSpec.describe Dependabot::Nuget::FileParser::PackagesConfigParser do - let(:file) do - Dependabot::DependencyFile.new(name: "packages.config", content: file_body) - end - let(:file_body) { fixture("packages_configs", "packages.config") } - let(:parser) { described_class.new(packages_config: file) } - - describe "dependency_set" do - subject(:dependency_set) { parser.dependency_set } - - it { is_expected.to be_a(Dependabot::FileParsers::Base::DependencySet) } - - describe "the dependencies" do - subject(:dependencies) { dependency_set.dependencies } - - its(:length) { is_expected.to eq(9) } - - describe "the first dependency" do - subject(:dependency) { dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.CodeDom.Providers.DotNetCompilerPlatform") - expect(dependency.version).to eq("1.0.0") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.0", - file: "packages.config", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "the second dependency" do - subject(:dependency) { dependencies.at(1) } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.Net.Compilers") - expect(dependency.version).to eq("1.0.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.1", - file: "packages.config", - groups: ["devDependencies"], - source: nil - }] - ) - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb deleted file mode 100644 index 35b486e47f4..00000000000 --- a/nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb +++ /dev/null @@ -1,1118 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency_file" -require "dependabot/source" -require "dependabot/nuget/cache_manager" -require "dependabot/nuget/file_parser/project_file_parser" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::FileParser::ProjectFileParser do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - let(:file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: file_body) - end - let(:file_body) { fixture("csproj", "basic.csproj") } - let(:parser) do - described_class.new(dependency_files: [file], credentials: credentials, repo_contents_path: "/test/repo") - end - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - - before do - # these search results are used by many tests; for these tests, the actual versions don't matter, it just matters - # that search returns _something_ - versions = ["2.2.2", "1.1.1", "1.0.0"] - stub_search_results_with_versions_v3("gitversion.commandline", versions) - stub_search_results_with_versions_v3("microsoft.aspnetcore.app", versions) - stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", versions) - stub_search_results_with_versions_v3("microsoft.extensions.platformabstractions", versions) - stub_search_results_with_versions_v3("microsoft.net.test.sdk", versions) - stub_search_results_with_versions_v3("microsoft.sourcelink.github", versions) - stub_search_results_with_versions_v3("newtonsoft.json", versions) - stub_search_results_with_versions_v3("nanoframework.corelibrary", versions) - stub_search_results_with_versions_v3("nuke.codegeneration", versions) - stub_search_results_with_versions_v3("nuke.common", versions) - stub_search_results_with_versions_v3("serilog", versions) - stub_search_results_with_versions_v3("system.collections.specialized", versions) - end - - describe "#downstream_file_references" do - subject(:downstream_file_references) { parser.downstream_file_references(project_file: file) } - - context "when there is no `Include` or `Update` attribute on the ``" do - let(:file_body) do - <<~XML - - - net8.0 - - - - - - - XML - end - - it "does not report that dependency" do - expect(downstream_file_references).to eq(Set["Some.Other.Project.csproj"]) - end - end - end - - describe "dependency_set" do - subject(:dependency_set) { parser.dependency_set(project_file: file) } - - before do - allow(parser).to receive(:transitive_dependencies_from_packages).and_return([]) - end - - it { is_expected.to be_a(Dependabot::FileParsers::Base::DependencySet) } - - describe "the transitive dependencies" do - let(:file_body) { fixture("csproj", "transitive_project_reference.csproj") } - let(:file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: file_body) - end - let(:files) do - [ - file, - Dependabot::DependencyFile.new( - name: "ref/another.csproj", - content: fixture("csproj", "transitive_referenced_project.csproj") - ) - ] - end - let(:parser) do - described_class.new(dependency_files: files, credentials: credentials, repo_contents_path: "/test/repo") - end - let(:dependencies) { dependency_set.dependencies } - subject(:transitive_dependencies) { dependencies.reject(&:top_level?) } - - its(:length) { is_expected.to eq(20) } - - def dependencies_from(dep_info) - dep_info.map do |info| - Dependabot::Dependency.new( - name: info[:name], - version: info[:version], - requirements: [], - package_manager: "nuget" - ) - end - end - - let(:raw_transitive_dependencies) do - [ - { name: "Microsoft.CSharp", version: "4.0.1" }, - { name: "System.Dynamic.Runtime", version: "4.0.11" }, - { name: "System.Linq.Expressions", version: "4.1.0" }, - { name: "System.Reflection", version: "4.1.0" }, - { name: "Microsoft.NETCore.Platforms", version: "1.0.1" }, - { name: "Microsoft.NETCore.Targets", version: "1.0.1" }, - { name: "System.IO", version: "4.1.0" }, - { name: "System.Runtime", version: "4.1.0" }, - { name: "System.Text.Encoding", version: "4.0.11" }, - { name: "System.Threading.Tasks", version: "4.0.11" }, - { name: "System.Reflection.Primitives", version: "4.0.1" }, - { name: "System.ObjectModel", version: "4.0.12" }, - { name: "System.Collections", version: "4.0.11" }, - { name: "System.Globalization", version: "4.0.11" }, - { name: "System.Linq", version: "4.1.0" }, - { name: "System.Reflection.Extensions", version: "4.0.1" }, - { name: "System.Runtime.Extensions", version: "4.1.0" }, - { name: "System.Text.RegularExpressions", version: "4.1.0" }, - { name: "System.Threading", version: "4.0.11" } - ] - end - - before do - allow(parser).to receive(:transitive_dependencies_from_packages).and_return( - dependencies_from(raw_transitive_dependencies) - ) - end - - describe "the referenced project dependencies" do - subject(:dependency) do - transitive_dependencies.find do |dep| - dep.name == "Microsoft.Extensions.DependencyModel" - end - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Microsoft.Extensions.DependencyModel") - expect(dependency.version).to eq("1.0.1") - expect(dependency.requirements).to eq([]) - end - end - end - - describe "dependencies from Directory.Packages.props" do - let(:parser) do - described_class.new(dependency_files: dependency_files, credentials: credentials, - repo_contents_path: "/test/repo") - end - let(:project_file) do - Dependabot::DependencyFile.new( - name: "project.csproj", - content: - <<~XML - - - net8.0 - - - - - - - XML - ) - end - let(:dependency_set) { parser.dependency_set(project_file: project_file) } - let(:dependency_files) do - [ - project_file, - Dependabot::DependencyFile.new( - name: "Directory.Packages.props", - content: - <<~XML - - - true - - - - - - - XML - ) - ] - end - - subject(:dependencies) { dependency_set.dependencies } - - before do - stub_search_results_with_versions_v3("some.package", ["1.2.3"]) - stub_search_results_with_versions_v3("some.other.package", ["4.5.6"]) - end - - it "returns the correct information" do - expect(dependencies.length).to eq(2) - - expect(dependencies[0]).to be_a(Dependabot::Dependency) - expect(dependencies[0].name).to eq("Some.Package") - expect(dependencies[0].version).to eq("$SomePropertyThatIsNotResolvable") - - expect(dependencies[1]).to be_a(Dependabot::Dependency) - expect(dependencies[1].name).to eq("Some.Other.Package") - expect(dependencies[1].version).to eq("4.5.6") - end - end - - describe "the top_level dependencies" do - let(:dependencies) { dependency_set.dependencies } - subject(:top_level_dependencies) { dependencies.select(&:top_level?) } - - its(:length) { is_expected.to eq(5) } - - before do - stub_search_results_with_versions_v3("system.askjeeves", ["1.0.0", "1.1.0"]) - stub_search_results_with_versions_v3("system.google", ["1.0.0", "1.1.0"]) - stub_search_results_with_versions_v3("system.lycos", ["1.0.0", "1.1.0"]) - stub_search_results_with_versions_v3("system.webcrawler", ["1.0.0", "1.1.0"]) - end - - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Microsoft.Extensions.DependencyModel") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "the second dependency" do - subject(:dependency) { top_level_dependencies[1] } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Microsoft.AspNetCore.App") - expect(dependency.version).to be_nil - expect(dependency.requirements).to eq( - [{ - requirement: nil, - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.Collections.Specialized") - expect(dependency.version).to eq("4.3.0") - expect(dependency.requirements).to eq( - [{ - requirement: "4.3.0", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - context "with version ranges" do - let(:file_body) { fixture("csproj", "ranges.csproj") } - - its(:length) { is_expected.to eq(6) } - - before do - stub_search_results_with_versions_v3("dep1", ["1.1.0", "1.2.0"]) - stub_search_results_with_versions_v3("dep2", ["1.1.0", "1.2.0"]) - stub_search_results_with_versions_v3("dep3", ["0.9.0", "1.0.0"]) - stub_search_results_with_versions_v3("dep4", ["1.0.0", "1.0.1"]) - stub_search_results_with_versions_v3("dep5", ["1.1.0", "1.2.0"]) - stub_search_results_with_versions_v3("dep6", ["1.1.0", "1.2.0"]) - end - - it "has the right details" do - expect(top_level_dependencies.first.requirements.first.fetch(:requirement)) - .to eq("[1.0,2.0]") - expect(top_level_dependencies.first.version).to be_nil - - expect(top_level_dependencies[1].requirements.first.fetch(:requirement)) - .to eq("[1.1]") - expect(top_level_dependencies[1].version).to eq("1.1") - - expect(top_level_dependencies[2].requirements.first.fetch(:requirement)) - .to eq("(,1.0)") - expect(top_level_dependencies[2].version).to be_nil - - expect(top_level_dependencies[3].requirements.first.fetch(:requirement)) - .to eq("1.0.*") - expect(top_level_dependencies[3].version).to be_nil - - expect(top_level_dependencies[4].requirements.first.fetch(:requirement)) - .to eq("*") - expect(top_level_dependencies[4].version).to be_nil - - expect(top_level_dependencies[5].requirements.first.fetch(:requirement)) - .to eq("*-*") - expect(top_level_dependencies[5].version).to be_nil - end - end - - context "with an update specified" do - let(:file_body) { fixture("csproj", "update.csproj") } - - it "has the right details" do - expect(top_level_dependencies.map(&:name)) - .to match_array( - %w( - Microsoft.Extensions.DependencyModel - Microsoft.AspNetCore.App - Microsoft.Extensions.PlatformAbstractions - System.Collections.Specialized - ) - ) - end - end - - context "with an updated package specified" do - let(:file_body) { fixture("csproj", "packages.props") } - - it "has the right details" do - expect(top_level_dependencies.map(&:name)) - .to match_array( - %w( - Microsoft.SourceLink.GitHub - System.AskJeeves - System.Google - System.Lycos - System.WebCrawler - ) - ) - end - end - - context "with an updated package specified" do - let(:file_body) { fixture("csproj", "directory.packages.props") } - - it "has the right details" do - expect(top_level_dependencies.map(&:name)) - .to match_array( - %w( - System.AskJeeves - System.Google - System.Lycos - System.WebCrawler - ) - ) - end - end - - context "with a property version" do - let(:file_body) do - fixture("csproj", "property_version.csproj") - end - - describe "the property dependency" do - subject(:dependency) do - top_level_dependencies.find { |d| d.name == "Nuke.Common" } - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Nuke.Common") - expect(dependency.version).to eq("0.1.434") - expect(dependency.requirements).to eq( - [{ - requirement: "0.1.434", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] - ) - end - end - - context "that is indirect" do - let(:file_body) do - fixture("csproj", "property_version_indirect.csproj") - end - - subject(:dependency) do - top_level_dependencies.find { |d| d.name == "Nuke.Uncommon" } - end - - before do - stub_search_results_with_versions_v3("nuke.uncommon", ["0.1.434"]) - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Nuke.Uncommon") - expect(dependency.version).to eq("0.1.434") - expect(dependency.requirements).to eq( - [{ - requirement: "0.1.434", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] - ) - end - end - - context "from a Directory.Build.props file several directories up" do - # src/my.csproj - let(:file_body) do - fixture("csproj", "property_version_not_in_file.csproj") - end - let(:file) do - Dependabot::DependencyFile.new(name: "src/my.csproj", content: file_body) - end - - # src/Directory.Build.props - let(:directory_build_props) do - Dependabot::DependencyFile.new(name: "src/Directory.Build.props", - content: fixture("csproj", - "directory_build_props_that_pulls_in_from_parent.props")) - end - - # Directory.Build.props - let(:root_directory_build_props) do - Dependabot::DependencyFile.new(name: "Directory.Build.props", - content: fixture("csproj", - "directory_build_props_with_property_version.props")) - end - - let(:files) do - [ - file, - directory_build_props, - root_directory_build_props - ] - end - - let(:parser) do - described_class.new(dependency_files: files, credentials: credentials, - repo_contents_path: "/test/repo") - end - - subject(:dependency) do - top_level_dependencies.find { |d| d.name == "Newtonsoft.Json" } - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Newtonsoft.Json") - expect(dependency.version).to eq("9.0.1") - expect(dependency.requirements).to eq( - [{ - requirement: "9.0.1", - file: "src/my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NewtonsoftJsonVersion" } - }] - ) - end - end - - context "from a Directory.Build.targets file several directories up" do - # src/my.csproj - let(:file_body) do - fixture("csproj", "property_version_not_in_file.csproj") - end - let(:file) do - Dependabot::DependencyFile.new(name: "src/my.csproj", content: file_body) - end - - # src/Directory.Build.targets - let(:directory_build_props) do - Dependabot::DependencyFile.new(name: "src/Directory.Build.targets", - content: fixture("csproj", - "directory_build_props_that_pulls_in_from_parent.props")) - end - - # Directory.Build.targets - let(:root_directory_build_props) do - Dependabot::DependencyFile.new(name: "Directory.Build.targets", - content: fixture("csproj", - "directory_build_props_with_property_version.props")) - end - - let(:files) do - [ - file, - directory_build_props, - root_directory_build_props - ] - end - - let(:parser) do - described_class.new(dependency_files: files, credentials: credentials, - repo_contents_path: "/test/repo") - end - - subject(:dependency) do - top_level_dependencies.find { |d| d.name == "Newtonsoft.Json" } - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Newtonsoft.Json") - expect(dependency.version).to eq("9.0.1") - expect(dependency.requirements).to eq( - [{ - requirement: "9.0.1", - file: "src/my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NewtonsoftJsonVersion" } - }] - ) - end - end - - context "from Directory.Build.props with an explicit update in Directory.Build.targets" do - # src/my.csproj - let(:file_body) do - fixture("csproj", "property_version_not_in_file.csproj") - end - let(:file) do - Dependabot::DependencyFile.new(name: "src/my.csproj", content: file_body) - end - - # src/Directory.Build.props - let(:directory_build_props) do - Dependabot::DependencyFile.new(name: "src/Directory.Build.props", - content: fixture("csproj", - "directory_build_props_that_pulls_in_from_parent.props")) - end - - # Directory.Build.props - let(:root_directory_build_props) do - Dependabot::DependencyFile.new(name: "Directory.Build.props", - content: fixture("csproj", - "directory_build_props_with_property_version.props")) - end - - # Directory.Build.targets - let(:root_directory_build_targets) do - Dependabot::DependencyFile.new(name: "Directory.Build.targets", - content: fixture("csproj", - "directory_build_props_with_package_update_variable.props")) - end - - let(:files) do - [ - file, - directory_build_props, - root_directory_build_props, - root_directory_build_targets - ] - end - - let(:parser) do - described_class.new(dependency_files: files, credentials: credentials, - repo_contents_path: "/test/repo") - end - - subject(:dependency) do - top_level_dependencies.find { |d| d.name == "Newtonsoft.Json" } - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Newtonsoft.Json") - expect(dependency.version).to eq("9.0.1") - expect(dependency.requirements).to eq( - [{ - requirement: "9.0.1", - file: "src/my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NewtonsoftJsonVersion" } - }, - { - requirement: "9.0.1", - file: "Directory.Build.targets", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NewtonsoftJsonVersion" } - }] - ) - end - end - - context "that can't be found" do - let(:file_body) do - fixture("csproj", "missing_property_version.csproj") - end - - describe "the property dependency" do - subject(:dependency) do - top_level_dependencies.find { |d| d.name == "Nuke.Common" } - end - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Nuke.Common") - expect(dependency.version).to eq("$UnknownVersion") - expect(dependency.requirements).to eq( - [{ - requirement: "$(UnknownVersion)", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "UnknownVersion" } - }] - ) - end - end - - describe "the dependency name" do - let(:file_body) do - fixture("csproj", "dependency_with_name_that_does_not_exist.csproj") - end - - before do - stub_no_search_results("this.dependency.does.not.exist") - end - - it "has the right details" do - expect(top_level_dependencies.count).to eq(1) - expect(top_level_dependencies.first).to be_a(Dependabot::Dependency) - expect(top_level_dependencies.first.name).to eq("Microsoft.Extensions.DependencyModel") - expect(top_level_dependencies.first.version).to eq("1.1.1") - expect(top_level_dependencies.first.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - # This is a bit of a noop now that we're moved off the query Nuget API, - # But we're keeping the test for completeness. - describe "the dependency name is a partial, but not perfect match" do - let(:file_body) do - fixture("csproj", "dependency_with_name_that_does_not_exist.csproj") - end - - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/this.dependency.does.not.exist/index.json") - .to_return(status: 404, body: "") - - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/this.dependency.does.not.exist_but.this.one.does") - .to_return(status: 200, body: registration_results( - "this.dependency.does.not.exist_but.this.one.does", ["1.0.0"] - )) - end - - it "has the right details" do - expect(top_level_dependencies.count).to eq(1) - expect(top_level_dependencies.first).to be_a(Dependabot::Dependency) - expect(top_level_dependencies.first.name).to eq("Microsoft.Extensions.DependencyModel") - expect(top_level_dependencies.first.version).to eq("1.1.1") - expect(top_level_dependencies.first.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "using non-standard nuget sources" do - let(:file_body) do - fixture("csproj", "dependency_with_name_that_does_not_exist.csproj") - end - let(:file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: file_body) - end - let(:nuget_config_body) do - <<~XML - - - - - - - - - XML - end - let(:nuget_config_file) do - Dependabot::DependencyFile.new(name: "NuGet.config", content: nuget_config_body) - end - let(:parser) do - described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials, - repo_contents_path: "/test/repo") - end - - before do - # no results - stub_request(:get, "https://no-results.api.example.com/v3/index.json") - .to_return(status: 200, body: fixture("nuget_responses", "index.json", - "no-results.api.example.com.index.json")) - stub_request(:get, "https://no-results.api.example.com/v3/registration5-gz-semver2/this.dependency.does.not.exist/index.json") - .to_return(status: 404, body: "") - stub_request(:get, "https://no-results.api.example.com/v3/registration5-gz-semver2/microsoft.extensions.dependencymodel/index.json") - .to_return(status: 404, body: "") - - # with results - stub_request(:get, "https://with-results.api.example.com/v3/index.json") - .to_return(status: 200, body: fixture("nuget_responses", "index.json", - "with-results.api.example.com.index.json")) - stub_request(:get, "https://with-results.api.example.com/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json") - .to_return(status: 200, body: registration_results("microsoft.extensions.dependencymodel", - ["1.1.1", "1.1.0"])) - stub_request(:get, "https://with-results.api.example.com/v3/registration5-gz-semver2/" \ - "this.dependency.does.not.exist/index.json") - .to_return(status: 404, body: "") - end - - it "has the right details" do - expect(top_level_dependencies.count).to eq(1) - expect(top_level_dependencies.first).to be_a(Dependabot::Dependency) - expect(top_level_dependencies.first.name).to eq("Microsoft.Extensions.DependencyModel") - expect(top_level_dependencies.first.version).to eq("1.1.1") - expect(top_level_dependencies.first.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "v2 apis can be queried" do - let(:file_body) do - <<~XML - - - net7.0 - - - - - - XML - end - let(:file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: file_body) - end - let(:nuget_config_body) do - <<~XML - - - - - - - - XML - end - let(:nuget_config_file) do - Dependabot::DependencyFile.new(name: "NuGet.config", content: nuget_config_body) - end - let(:parser) do - described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials, - repo_contents_path: "/test/repo") - end - - before do - stub_request(:get, "https://www.nuget.org/api/v2/") - .to_return(status: 200, body: fixture("nuget_responses", "v2_base.xml")) - stub_request(:get, "https://www.nuget.org/api/v2/FindPackagesById()?id=%27Microsoft.Extensions.DependencyModel%27") - .to_return(status: 200, body: search_results_with_versions_v2("microsoft.extensions.dependencymodel", - ["1.1.1", "1.1.0"])) - end - - it "has the right details" do - expect(top_level_dependencies.count).to eq(1) - expect(top_level_dependencies.first).to be_a(Dependabot::Dependency) - expect(top_level_dependencies.first.name).to eq("Microsoft.Extensions.DependencyModel") - expect(top_level_dependencies.first.version).to eq("1.1.1") - expect(top_level_dependencies.first.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "nuget.config files further up the tree are considered" do - let(:file_body) { "not relevant" } - let(:file) do - Dependabot::DependencyFile.new(directory: "src/project", name: "my.csproj", content: file_body) - end - let(:nuget_config_body) { "not relevant" } - let(:nuget_config_file) do - Dependabot::DependencyFile.new(name: "../../NuGet.Config", content: nuget_config_body) - end - let(:parser) do - described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials, - repo_contents_path: "/test/repo") - end - - it "finds the config file up several directories" do - nuget_configs = parser.nuget_configs - expect(nuget_configs.count).to eq(1) - expect(nuget_configs.first).to be_a(Dependabot::DependencyFile) - expect(nuget_configs.first.name).to eq("../../NuGet.Config") - end - end - - describe "files with a `nuget.config` suffix are not considered" do - let(:file_body) { "not relevant" } - let(:file) do - Dependabot::DependencyFile.new(directory: "src/project", name: "my.csproj", content: file_body) - end - let(:nuget_config_body) { "not relevant" } - let(:nuget_config_file) do - Dependabot::DependencyFile.new(name: "../../not-NuGet.Config", content: nuget_config_body) - end - let(:parser) do - described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials, - repo_contents_path: "/test/repo") - end - - it "does not return a name with a partial match" do - nuget_configs = parser.nuget_configs - expect(nuget_configs.count).to eq(0) - end - end - - describe "multiple dependencies, but each search URI is only hit once" do - let(:file_body) do - <<~XML - - - net7.0 - - - - - - - XML - end - let(:file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: file_body) - end - let(:file_2_body) do - <<~XML - - - net7.0 - - - - - - XML - end - let(:file2) do - Dependabot::DependencyFile.new(name: "my2.csproj", content: file_2_body) - end - let(:parser) do - described_class.new(dependency_files: [file, file2], credentials: credentials, - repo_contents_path: "/test/repo") - end - - before do - stub_no_search_results("this.dependency.does.not.exist") - ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" - end - - it "has the right details" do - ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" - - registry_stub = stub_registry_v3("microsoft.extensions.dependencymodel_cached", ["1.1.1", "1.1.0"]) - - expect(top_level_dependencies.count).to eq(1) - expect(top_level_dependencies.first).to be_a(Dependabot::Dependency) - expect(top_level_dependencies.first.name).to eq("Microsoft.Extensions.DependencyModel_cached") - expect(top_level_dependencies.first.version).to eq("1.1.1") - expect(top_level_dependencies.first.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) - expect(WebMock::RequestRegistry.instance.times_executed(registry_stub.request_pattern)).to eq(1) - ensure - ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" - Dependabot::Nuget::CacheManager.instance_variable_set(:@cache, nil) - end - - after do - ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" - end - end - end - end - - context "with a nuproj" do - let(:file_body) { fixture("csproj", "basic.nuproj") } - - before do - stub_search_results_with_versions_v3("nanoframework.coreextra", ["1.0.0"]) - end - - it "gets the right number of dependencies" do - expect(top_level_dependencies.count).to eq(2) - end - - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("nanoFramework.CoreLibrary") - expect(dependency.version).to eq("1.0.0-preview062") - expect(dependency.requirements).to eq([{ - requirement: "[1.0.0-preview062]", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }]) - end - end - - describe "the second dependency" do - subject(:dependency) { top_level_dependencies.at(1) } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("nanoFramework.CoreExtra") - expect(dependency.version).to eq("1.0.0-preview061") - expect(dependency.requirements).to eq([{ - requirement: "[1.0.0-preview061]", - file: "my.csproj", - groups: ["devDependencies"], - source: nil - }]) - end - end - end - - context "with an interpolated value" do - let(:file_body) { fixture("csproj", "interpolated.proj") } - - it "excludes the dependencies specified using interpolation" do - expect(top_level_dependencies.count).to eq(0) - end - end - - context "with a versioned sdk reference" do - before do - stub_search_results_with_versions_v3("awesome.sdk", ["1.2.3"]) - stub_search_results_with_versions_v3("prototype.sdk", ["1.2.3"]) - end - - context "specified in the Project tag" do - let(:file_body) { fixture("csproj", "sdk_reference_via_project.csproj") } - - its(:length) { is_expected.to eq(2) } - - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Awesome.Sdk") - expect(dependency.version).to eq("1.2.3") - expect(dependency.requirements).to eq([{ - requirement: "1.2.3", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }]) - end - end - - describe "the second dependency" do - subject(:dependency) { top_level_dependencies[1] } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Prototype.Sdk") - expect(dependency.version).to eq("0.1.0-beta") - expect(dependency.requirements).to eq([{ - requirement: "0.1.0-beta", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }]) - end - end - end - - context "specified via an Sdk tag" do - let(:file_body) { fixture("csproj", "sdk_reference_via_sdk.csproj") } - - its(:length) { is_expected.to eq(2) } - - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Awesome.Sdk") - expect(dependency.version).to eq("1.2.3") - expect(dependency.requirements).to eq([{ - requirement: "1.2.3", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }]) - end - end - - describe "the second dependency" do - subject(:dependency) { top_level_dependencies[1] } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Prototype.Sdk") - expect(dependency.version).to eq("0.1.0-beta") - expect(dependency.requirements).to eq([{ - requirement: "0.1.0-beta", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }]) - end - end - end - - context "specified via an Import tag" do - let(:file_body) { fixture("csproj", "sdk_reference_via_import.csproj") } - - its(:length) { is_expected.to eq(2) } - - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Awesome.Sdk") - expect(dependency.version).to eq("1.2.3") - expect(dependency.requirements).to eq([{ - requirement: "1.2.3", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }]) - end - end - - describe "the second dependency" do - subject(:dependency) { top_level_dependencies[1] } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Prototype.Sdk") - expect(dependency.version).to eq("0.1.0-beta") - expect(dependency.requirements).to eq([{ - requirement: "0.1.0-beta", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }]) - end - end - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/file_parser/property_value_finder_spec.rb b/nuget/spec/dependabot/nuget/file_parser/property_value_finder_spec.rb deleted file mode 100644 index 998cddb0c5f..00000000000 --- a/nuget/spec/dependabot/nuget/file_parser/property_value_finder_spec.rb +++ /dev/null @@ -1,206 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency_file" -require "dependabot/source" -require "dependabot/nuget/file_parser/property_value_finder" - -RSpec.describe Dependabot::Nuget::FileParser::PropertyValueFinder do - let(:file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: file_body) - end - let(:file_body) { fixture("csproj", csproj_fixture_name) } - let(:csproj_fixture_name) { "property_version.csproj" } - let(:finder) { described_class.new(dependency_files: files) } - let(:files) { [file] } - let(:property_name) { "NukeVersion" } - - describe "property_details" do - subject(:property_details) do - finder.property_details(property_name: property_name, callsite_file: file) - end - - context "with a property that can be found" do - let(:property_name) { "NukeVersion" } - its([:value]) { is_expected.to eq("0.1.434") } - its([:file]) { is_expected.to eq("my.csproj") } - its([:root_property_name]) { is_expected.to eq("NukeVersion") } - - context "but which calls another property" do - let(:csproj_fixture_name) { "property_version_indirect.csproj" } - let(:property_name) { "IndirectNukeVersion" } - its([:value]) { is_expected.to eq("0.1.434") } - its([:file]) { is_expected.to eq("my.csproj") } - its([:root_property_name]) { is_expected.to eq("NukeVersion") } - - context "leading to an infinite loop" do - let(:property_name) { "LoopOne" } - - # For now we should manually investigate if this ever happens - it "raises" do - expect { property_details }.to raise_error("Circular reference!") - end - end - end - end - - context "with a property that can't be found" do - let(:property_name) { "UnknownVersion" } - it { is_expected.to be_nil } - end - - context "with a property that calls a function" do - let(:property_name) { "FunctionVersion" } - it { is_expected.to be_nil } - end - - context "with properties both with and without conditions" do - let(:csproj_fixture_name) { "properties_with_conditions.csproj" } - let(:property_name) { "NewtonsoftJsonVersion" } - its([:value]) { is_expected.to eq("12.0.1") } - end - - context "from a directory.build.props file" do - let(:files) { [file, build_file, imported_file] } - - let(:file) do - Dependabot::DependencyFile.new( - name: "nested/my.csproj", - content: file_body - ) - end - let(:file_body) { fixture("csproj", "property_version.csproj") } - let(:build_file) do - Dependabot::DependencyFile.new( - name: "Directory.Build.props", - content: build_file_body - ) - end - let(:build_file_body) { fixture("property_files", "imports") } - let(:imported_file) do - Dependabot::DependencyFile.new( - name: "build/dependencies.props", - content: imported_file_body - ) - end - let(:imported_file_body) do - fixture("property_files", "dependency.props") - end - - let(:property_name) { "XunitPackageVersion" } - - its([:value]) { is_expected.to eq("2.3.1") } - its([:file]) { is_expected.to eq("build/dependencies.props") } - its([:root_property_name]) { is_expected.to eq("XunitPackageVersion") } - end - - context "from a directory.build.targets file" do - let(:files) { [file, build_file, imported_file] } - - let(:file) do - Dependabot::DependencyFile.new( - name: "nested/my.csproj", - content: file_body - ) - end - let(:file_body) { fixture("csproj", "property_version.csproj") } - let(:build_file) do - Dependabot::DependencyFile.new( - name: "Directory.Build.targets", - content: build_file_body - ) - end - let(:build_file_body) { fixture("property_files", "imports") } - let(:imported_file) do - Dependabot::DependencyFile.new( - name: "build/dependencies.props", - content: imported_file_body - ) - end - let(:imported_file_body) do - fixture("property_files", "dependency.props") - end - - let(:property_name) { "XunitPackageVersion" } - - its([:value]) { is_expected.to eq("2.3.1") } - its([:file]) { is_expected.to eq("build/dependencies.props") } - its([:root_property_name]) { is_expected.to eq("XunitPackageVersion") } - end - - context "from a packages.props file" do - let(:files) { [file, build_file, imported_file] } - - let(:file) do - Dependabot::DependencyFile.new( - name: "nested/my.csproj", - content: file_body - ) - end - let(:file_body) { fixture("csproj", "property_version.csproj") } - let(:build_file) do - Dependabot::DependencyFile.new( - name: "Packages.props", - content: build_file_body - ) - end - let(:build_file_body) { fixture("property_files", "packages.props") } - let(:imported_file) do - Dependabot::DependencyFile.new( - name: "build/dependencies.props", - content: imported_file_body - ) - end - let(:imported_file_body) do - fixture("property_files", "dependency.props") - end - - let(:property_name) { "SystemSearchEngineVersion" } - - its([:value]) { is_expected.to eq("3.0.0-alpha1-10221") } - its([:file]) { is_expected.to eq("Packages.props") } - its([:root_property_name]) do - is_expected.to eq("SystemSearchEngineVersion") - end - end - - context "from a directory.packages.props file" do - let(:files) { [file, build_file, imported_file] } - - let(:file) do - Dependabot::DependencyFile.new( - name: "nested/my.csproj", - content: file_body - ) - end - let(:file_body) { fixture("csproj", "property_version.csproj") } - let(:build_file) do - Dependabot::DependencyFile.new( - name: "Directory.Packages.props", - content: build_file_body - ) - end - let(:build_file_body) do - fixture("property_files", "directory.packages.props") - end - let(:imported_file) do - Dependabot::DependencyFile.new( - name: "build/dependencies.props", - content: imported_file_body - ) - end - let(:imported_file_body) do - fixture("property_files", "dependency.props") - end - - let(:property_name) { "SystemSearchEngineVersion" } - - its([:value]) { is_expected.to eq("3.0.0-alpha1-10221") } - its([:file]) { is_expected.to eq("Directory.Packages.props") } - its([:root_property_name]) do - is_expected.to eq("SystemSearchEngineVersion") - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index 088f77ef4d0..de8ca2492c9 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -5,6 +5,7 @@ require "dependabot/dependency_file" require "dependabot/source" require "dependabot/nuget/file_parser" +require "dependabot/nuget/version" require_relative "nuget_search_stubs" require_common_spec "file_parsers/shared_examples_for_file_parsers" @@ -14,13 +15,16 @@ end it_behaves_like "a dependency file parser" - - let(:files) { [csproj_file] } - let(:csproj_file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) + let(:project_name) { "file_parser_csproj" } + let(:directory) { "/" } + # project_dependency files comes back with directory files first, we need the closest project at the top + let(:files) { nuget_project_dependency_files(project_name, directory: directory).reverse } + let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } + let(:parser) do + described_class.new(dependency_files: files, + source: source, + repo_contents_path: repo_contents_path) end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - let(:parser) { described_class.new(dependency_files: files, source: source) } let(:source) do Dependabot::Source.new( provider: "github", @@ -54,13 +58,7 @@ def dependencies_from_info(deps_info) subject(:top_level_dependencies) { dependencies.select(&:top_level?) } context "with a .proj file" do - let(:files) { [proj_file] } - let(:proj_file) do - Dependabot::DependencyFile.new( - name: "proj.proj", - content: fixture("csproj", "basic2.csproj") - ) - end + let(:project_name) { "file_parser_proj" } let(:proj_dependencies) do [ @@ -69,17 +67,10 @@ def dependencies_from_info(deps_info) ] end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - allow(dummy_project_file_parser).to receive(:dependency_set).with(project_file: proj_file).and_return( - dependencies_from_info(proj_dependencies) - ) - end its(:length) { is_expected.to eq(2) } - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } + describe "the Microsoft.Extensions.DependencyModel dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -96,8 +87,8 @@ def dependencies_from_info(deps_info) end end - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } + describe "the Serilog dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Serilog" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -126,17 +117,10 @@ def dependencies_from_info(deps_info) ] end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - allow(dummy_project_file_parser).to receive(:dependency_set).and_return( - dependencies_from_info(project_dependencies) - ) - end its(:length) { is_expected.to eq(5) } - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } + describe "the Microsoft.Extensions.DependencyModel dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -153,8 +137,8 @@ def dependencies_from_info(deps_info) end end - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } + describe "the System.Collections.Specialized dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "System.Collections.Specialized" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -173,13 +157,7 @@ def dependencies_from_info(deps_info) end context "with a csproj and a vbproj" do - let(:files) { [csproj_file, vbproj_file] } - let(:vbproj_file) do - Dependabot::DependencyFile.new( - name: "my.vbproj", - content: fixture("csproj", "basic2.csproj") - ) - end + let(:project_name) { "file_parser_csproj_vbproj" } let(:csproj_dependencies) do [ @@ -198,20 +176,10 @@ def dependencies_from_info(deps_info) ] end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - allow(dummy_project_file_parser).to receive(:dependency_set).with(project_file: csproj_file).and_return( - dependencies_from_info(csproj_dependencies) - ) - allow(dummy_project_file_parser).to receive(:dependency_set).with(project_file: vbproj_file).and_return( - dependencies_from_info(vbproj_dependencies) - ) - end its(:length) { is_expected.to eq(6) } - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } + describe "the Microsoft.Extensions.DependencyModel dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -233,8 +201,8 @@ def dependencies_from_info(deps_info) end end - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } + describe "the Serilog dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Serilog" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -253,18 +221,16 @@ def dependencies_from_info(deps_info) end context "with a packages.config" do - let(:files) { [packages_config] } - let(:packages_config) do - Dependabot::DependencyFile.new( - name: "packages.config", - content: fixture("packages_configs", "packages.config") - ) - end + let(:project_name) { "file_parser_packages_config" } its(:length) { is_expected.to eq(9) } - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } + describe "the Microsoft.CodeDom.Providers.DotNetCompilerPlatform dependency" do + subject(:dependency) do + dependencies.find do |d| + d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" + end + end it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -282,8 +248,8 @@ def dependencies_from_info(deps_info) end end - describe "the second dependency" do - subject(:dependency) { top_level_dependencies.at(1) } + describe "the Microsoft.Net.Compilers dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Net.Compilers" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -302,16 +268,15 @@ def dependencies_from_info(deps_info) end context "that is nested" do + let(:project_name) { "file_parser_packages_config_nested" } its(:length) { is_expected.to eq(9) } - let(:packages_config) do - Dependabot::DependencyFile.new( - name: "dir/packages.config", - content: fixture("packages_configs", "packages.config") - ) - end - describe "the first dependency" do - subject(:dependency) { top_level_dependencies.first } + describe "the Microsoft.CodeDom.Providers.DotNetCompilerPlatform dependency" do + subject(:dependency) do + dependencies.find do |d| + d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" + end + end it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -329,8 +294,8 @@ def dependencies_from_info(deps_info) end end - describe "the second dependency" do - subject(:dependency) { top_level_dependencies.at(1) } + describe "the Microsoft.Net.Compilers dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Net.Compilers" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -351,24 +316,12 @@ def dependencies_from_info(deps_info) end context "with a global.json" do - let(:files) { [packages_config, global_json] } - let(:packages_config) do - Dependabot::DependencyFile.new( - name: "packages.config", - content: fixture("packages_configs", "packages.config") - ) - end - let(:global_json) do - Dependabot::DependencyFile.new( - name: "global.json", - content: fixture("global_jsons", "global.json") - ) - end + let(:project_name) { "file_parser_packages_config_global_json" } its(:length) { is_expected.to eq(10) } - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } + describe "the Microsoft.Build.Traversal dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Build.Traversal" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -387,24 +340,12 @@ def dependencies_from_info(deps_info) end context "with a dotnet-tools.json" do - let(:files) { [packages_config, dotnet_tools_json] } - let(:packages_config) do - Dependabot::DependencyFile.new( - name: "packages.config", - content: fixture("packages_configs", "packages.config") - ) - end - let(:dotnet_tools_json) do - Dependabot::DependencyFile.new( - name: ".config/dotnet-tools.json", - content: fixture("dotnet_tools_jsons", "dotnet-tools.json") - ) - end + let(:project_name) { "file_parser_packages_config_dotnet_tools_json" } its(:length) { is_expected.to eq(11) } - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } + describe "the dotnetsay dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "dotnetsay" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -423,13 +364,7 @@ def dependencies_from_info(deps_info) end context "with an imported properties file" do - let(:files) { [csproj_file, imported_file] } - let(:imported_file) do - Dependabot::DependencyFile.new( - name: "commonprops.props", - content: fixture("csproj", "commonprops.props") - ) - end + let(:project_name) { "file_parser_csproj_imported_props" } let(:csproj_dependencies) do [ @@ -448,21 +383,10 @@ def dependencies_from_info(deps_info) ] end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: csproj_file).and_return( - dependencies_from_info(csproj_dependencies) - ) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: imported_file).and_return( - dependencies_from_info(imported_file_dependencies) - ) - end - its(:length) { is_expected.to eq(6) } - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } + describe "the Serilog dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Serilog" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -481,13 +405,7 @@ def dependencies_from_info(deps_info) end context "with a packages.props file" do - let(:files) { [csproj_file, packages_file] } - let(:packages_file) do - Dependabot::DependencyFile.new( - name: "packages.props", - content: fixture("csproj", "packages.props") - ) - end + let(:project_name) { "file_parser_csproj_packages_props" } let(:csproj_dependencies) do [ @@ -499,6 +417,12 @@ def dependencies_from_info(deps_info) ] end + let(:directory_build_dependencies) do + [ + { name: "Microsoft.Build.CentralPackageVersions", version: "2.1.3", file: "Directory.Build.targets" } + ] + end + let(:packages_file_dependencies) do [ { name: "Microsoft.SourceLink.GitHub", version: "1.0.0-beta2-19367-01", file: "packages.props" }, @@ -509,21 +433,10 @@ def dependencies_from_info(deps_info) ] end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: csproj_file).and_return( - dependencies_from_info(csproj_dependencies) - ) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: packages_file).and_return( - dependencies_from_info(packages_file_dependencies) - ) - end - - its(:length) { is_expected.to eq(10) } + its(:length) { is_expected.to eq(11) } - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } + describe "the System.WebCrawler dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "System.WebCrawler" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -532,7 +445,7 @@ def dependencies_from_info(deps_info) expect(dependency.requirements).to eq( [{ requirement: "1.1.1", - file: "packages.props", + file: "Packages.props", groups: ["dependencies"], source: nil }] @@ -542,13 +455,7 @@ def dependencies_from_info(deps_info) end context "with a directory.packages.props file" do - let(:files) { [csproj_file, packages_file] } - let(:packages_file) do - Dependabot::DependencyFile.new( - name: "directory.packages.props", - content: fixture("csproj", "directory.packages.props") - ) - end + let(:project_name) { "file_parser_csproj_directory_packages_props" } let(:csproj_dependencies) do [ @@ -569,21 +476,10 @@ def dependencies_from_info(deps_info) ] end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: csproj_file).and_return( - dependencies_from_info(csproj_dependencies) - ) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: packages_file).and_return( - dependencies_from_info(packages_file_dependencies) - ) - end - its(:length) { is_expected.to eq(9) } - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } + describe "the System.WebCrawler dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "System.WebCrawler" } } it "has the right details" do expect(dependency).to be_a(Dependabot::Dependency) @@ -592,54 +488,7 @@ def dependencies_from_info(deps_info) expect(dependency.requirements).to eq( [{ requirement: "1.1.1", - file: "directory.packages.props", - groups: ["dependencies"], - source: nil - }] - ) - end - end - end - - context "with only directory.packages.props file" do - let(:files) { [packages_file] } - let(:packages_file) do - Dependabot::DependencyFile.new( - name: "directory.packages.props", - content: fixture("csproj", "directory.packages.props") - ) - end - - let(:packages_file_dependencies) do - [ - { name: "Microsoft.SourceLink.GitHub", version: "1.0.0-beta2-19367-01", file: "directory.packages.props" }, - { name: "System.Lycos", version: "3.23.3", file: "directory.packages.props" }, - { name: "System.AskJeeves", version: "2.2.2", file: "directory.packages.props" }, - { name: "System.WebCrawler", version: "1.1.1", file: "directory.packages.props" } - ] - end - - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: packages_file).and_return( - dependencies_from_info(packages_file_dependencies) - ) - end - - its(:length) { is_expected.to eq(4) } - - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.WebCrawler") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "directory.packages.props", + file: "Directory.Packages.props", groups: ["dependencies"], source: nil }] @@ -649,23 +498,7 @@ def dependencies_from_info(deps_info) end context "with unparsable dependency versions" do - let(:csproj_file) do - Dependabot::DependencyFile.new( - name: "my.csproj", - content: - <<~XML - - - net8.0 - - - - - - - XML - ) - end + let(:project_name) { "file_parser_csproj_unparsable" } before do allow(Dependabot.logger).to receive(:warn) @@ -675,28 +508,34 @@ def dependencies_from_info(deps_info) .to_return( status: 200, body: - <<~XML - - - Package.A - 1.2.3 - - - - - - - XML + <<~XML + + + Package.A + 1.2.3 + + + + + + + XML ) end - it "returns only actionable dependencies" do - expect(dependencies.length).to eq(1) - expect(dependencies[0].name).to eq("Package.A") - expect(dependencies[0].version).to eq("1.2.3") - expect(Dependabot.logger).to have_received(:warn).with( - "Dependency 'Package.B' excluded due to unparsable version: $ThisPropertyCannotBeResolved" - ) + its(:length) { is_expected.to eq(1) } + + describe "the Package.A dependency" do + subject(:dependency) { dependencies.find { |d| d.name == "Package.A" } } + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("Package.A") + expect(dependency.version).to eq("1.2.3") + expect(Dependabot.logger).to have_received(:warn).with( + "Dependency 'Package.B' excluded due to unparsable version: $(ThisPropertyCannotBeResolved)" + ) + end end end end diff --git a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb index d6533e46d08..54ddc9d02c6 100644 --- a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb @@ -5,16 +5,19 @@ require "dependabot/dependency" require "dependabot/dependency_file" require "dependabot/nuget/update_checker/tfm_finder" +require "dependabot/nuget/file_parser" RSpec.describe Dependabot::Nuget::TfmFinder do - subject(:finder) do - described_class.new( - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: "test/repo" + let(:project_name) { "tfm_finder" } + let(:dependency_files) { nuget_project_dependency_files(project_name, directory: "/").reverse } + let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" ) end - let(:dependency) do Dependabot::Dependency.new( name: dependency_name, @@ -24,31 +27,11 @@ ) end - let(:dependency_files) { [exe_proj, lib_proj] } - let(:exe_proj) do - Dependabot::DependencyFile.new( - name: "my.csproj", - content: fixture("csproj", "transitive_project_reference.csproj") - ) - end - let(:lib_proj) do - Dependabot::DependencyFile.new( - name: "ref/another.csproj", - content: fixture("csproj", "transitive_referenced_project.csproj") - ) - end - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - - before do - allow(finder).to receive(:project_file_contains_dependency?).with(exe_proj, any_args).and_return(true) + subject(:frameworks) do + Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path).parse + Dependabot::Nuget::TfmFinder.frameworks(dependency) end describe "#frameworks" do @@ -57,12 +40,6 @@ let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } let(:dependency_version) { "1.1.1" } - subject(:frameworks) { finder.frameworks(dependency) } - - before do - allow(finder).to receive(:project_file_contains_dependency?).with(lib_proj, dependency).and_return(true) - end - its(:length) { is_expected.to eq(2) } end @@ -73,12 +50,6 @@ let(:dependency_name) { "Serilog" } let(:dependency_version) { "2.3.0" } - subject(:frameworks) { finder.frameworks(dependency) } - - before do - allow(finder).to receive(:project_file_contains_dependency?).with(lib_proj, dependency).and_return(false) - end - its(:length) { is_expected.to eq(1) } end end diff --git a/nuget/spec/fixtures/csproj/basic.csproj b/nuget/spec/fixtures/csproj/basic.csproj index 8c7bc675b22..c0c184bb7df 100644 --- a/nuget/spec/fixtures/csproj/basic.csproj +++ b/nuget/spec/fixtures/csproj/basic.csproj @@ -1,7 +1,7 @@ Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. - netstandard1.6;net452 + netstandard1.6;net462 @@ -16,7 +16,7 @@ 4.3.0 - + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj/my.csproj b/nuget/spec/fixtures/projects/file_parser_csproj/my.csproj new file mode 100644 index 00000000000..c0c184bb7df --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj/my.csproj @@ -0,0 +1,22 @@ + + + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + netstandard1.6;net462 + + + + + + + + + + + + 4.3.0 + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_directory_packages_props/Directory.Packages.props b/nuget/spec/fixtures/projects/file_parser_csproj_directory_packages_props/Directory.Packages.props new file mode 100644 index 00000000000..d5d3f0f2001 --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_directory_packages_props/Directory.Packages.props @@ -0,0 +1,11 @@ + + + true + + + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_directory_packages_props/my.csproj b/nuget/spec/fixtures/projects/file_parser_csproj_directory_packages_props/my.csproj new file mode 100644 index 00000000000..c0c184bb7df --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_directory_packages_props/my.csproj @@ -0,0 +1,22 @@ + + + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + netstandard1.6;net462 + + + + + + + + + + + + 4.3.0 + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_imported_props/commonprops.props b/nuget/spec/fixtures/projects/file_parser_csproj_imported_props/commonprops.props new file mode 100644 index 00000000000..a0ebe21fdc0 --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_imported_props/commonprops.props @@ -0,0 +1,5 @@ + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_imported_props/my.csproj b/nuget/spec/fixtures/projects/file_parser_csproj_imported_props/my.csproj new file mode 100644 index 00000000000..3882de3d1df --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_imported_props/my.csproj @@ -0,0 +1,24 @@ + + + + + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + netstandard1.6;net462 + + + + + + + + + + + + 4.3.0 + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/Directory.Build.targets b/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/Directory.Build.targets new file mode 100644 index 00000000000..3b6a67815ab --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/Packages.props b/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/Packages.props new file mode 100644 index 00000000000..df0714da719 --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/Packages.props @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/my.csproj b/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/my.csproj new file mode 100644 index 00000000000..c0c184bb7df --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_packages_props/my.csproj @@ -0,0 +1,22 @@ + + + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + netstandard1.6;net462 + + + + + + + + + + + + 4.3.0 + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_unparsable/my.csproj b/nuget/spec/fixtures/projects/file_parser_csproj_unparsable/my.csproj new file mode 100644 index 00000000000..b2358bdf1bf --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_unparsable/my.csproj @@ -0,0 +1,9 @@ + + + net8.0 + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_vbproj/my.csproj b/nuget/spec/fixtures/projects/file_parser_csproj_vbproj/my.csproj new file mode 100644 index 00000000000..c0c184bb7df --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_vbproj/my.csproj @@ -0,0 +1,22 @@ + + + Nancy is a lightweight web framework for the .Net platform, inspired by Sinatra. Nancy aim at delivering a low ceremony approach to building light, fast web applications. + netstandard1.6;net462 + + + + + + + + + + + + 4.3.0 + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_vbproj/my.vbproj b/nuget/spec/fixtures/projects/file_parser_csproj_vbproj/my.vbproj new file mode 100644 index 00000000000..b36663f7db7 --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_vbproj/my.vbproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config/empty.csproj b/nuget/spec/fixtures/projects/file_parser_packages_config/empty.csproj new file mode 100644 index 00000000000..8c119d5413b --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config/empty.csproj @@ -0,0 +1,2 @@ + + diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config/packages.config b/nuget/spec/fixtures/projects/file_parser_packages_config/packages.config new file mode 100644 index 00000000000..17da22716ae --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/.config/dotnet-tools.json b/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/.config/dotnet-tools.json new file mode 100644 index 00000000000..e943abb8b41 --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "botsay": { + "version": "1.0.0", + "commands": [ + "botsay" + ] + }, + "dotnetsay": { + "version": "1.0.0", + "commands": [ + "dotnetsay" + ] + } + } +} diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/empty.csproj b/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/empty.csproj new file mode 100644 index 00000000000..8c119d5413b --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/empty.csproj @@ -0,0 +1,2 @@ + + diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/packages.config b/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/packages.config new file mode 100644 index 00000000000..17da22716ae --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config_dotnet_tools_json/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/empty.csproj b/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/empty.csproj new file mode 100644 index 00000000000..8c119d5413b --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/empty.csproj @@ -0,0 +1,2 @@ + + diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/global.json b/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/global.json new file mode 100644 index 00000000000..24751e2e19b --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/global.json @@ -0,0 +1,8 @@ +{ + "sdk": { + "version": "2.2.104" + }, + "msbuild-sdks": { + "Microsoft.Build.Traversal": "1.0.45" + } +} diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/packages.config b/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/packages.config new file mode 100644 index 00000000000..17da22716ae --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config_global_json/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config_nested/dir/empty.csproj b/nuget/spec/fixtures/projects/file_parser_packages_config_nested/dir/empty.csproj new file mode 100644 index 00000000000..8c119d5413b --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config_nested/dir/empty.csproj @@ -0,0 +1,2 @@ + + diff --git a/nuget/spec/fixtures/projects/file_parser_packages_config_nested/dir/packages.config b/nuget/spec/fixtures/projects/file_parser_packages_config_nested/dir/packages.config new file mode 100644 index 00000000000..17da22716ae --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_packages_config_nested/dir/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/nuget/spec/fixtures/projects/file_parser_proj/proj.proj b/nuget/spec/fixtures/projects/file_parser_proj/proj.proj new file mode 100644 index 00000000000..b36663f7db7 --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_proj/proj.proj @@ -0,0 +1,6 @@ + + + + + + diff --git a/nuget/spec/fixtures/projects/tfm_finder/my.csproj b/nuget/spec/fixtures/projects/tfm_finder/my.csproj new file mode 100644 index 00000000000..e988b8053e5 --- /dev/null +++ b/nuget/spec/fixtures/projects/tfm_finder/my.csproj @@ -0,0 +1,10 @@ + + + net5.0 + + + + + + + diff --git a/nuget/spec/fixtures/projects/tfm_finder/ref/another.csproj b/nuget/spec/fixtures/projects/tfm_finder/ref/another.csproj new file mode 100644 index 00000000000..cc13502943e --- /dev/null +++ b/nuget/spec/fixtures/projects/tfm_finder/ref/another.csproj @@ -0,0 +1,9 @@ + + + netstandard2.0 + + + + + + From cc2ace07605adc11ec75120cea5972abd52267a0 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sat, 23 Mar 2024 09:10:52 -0700 Subject: [PATCH 17/26] Fix up remaining tests --- common/spec/spec_helper.rb | 26 +++++++++++++++++++ .../dependabot/nuget/file_updater_spec.rb | 22 +++++++++++----- .../compatibility_checker_spec.rb | 13 +++++++++- .../nuget/update_checker/tfm_finder_spec.rb | 2 +- .../update_checker/version_finder_spec.rb | 14 +++++++++- .../Proj1/Proj1/Proj1.csproj | 0 .../Proj1/dirs.proj | 0 .../Proj2/dirs.proj | 0 .../dirs.proj | 0 .../Proj1/Proj1/Proj1.csproj | 0 .../Proj1/dirs.proj | 0 .../Proj2/Proj2.csproj | 0 .../Proj2/dirs.proj | 0 .../dirs.proj | 0 14 files changed, 68 insertions(+), 9 deletions(-) rename nuget/spec/fixtures/projects/{dirsproj => file_updater_dirsproj}/Proj1/Proj1/Proj1.csproj (100%) rename nuget/spec/fixtures/projects/{dirsproj => file_updater_dirsproj}/Proj1/dirs.proj (100%) rename nuget/spec/fixtures/projects/{dirsproj => file_updater_dirsproj}/Proj2/dirs.proj (100%) rename nuget/spec/fixtures/projects/{dirsproj => file_updater_dirsproj}/dirs.proj (100%) rename nuget/spec/fixtures/projects/{dirsproj_wildcards => file_updater_dirsproj_wildcards}/Proj1/Proj1/Proj1.csproj (100%) rename nuget/spec/fixtures/projects/{dirsproj_wildcards => file_updater_dirsproj_wildcards}/Proj1/dirs.proj (100%) rename nuget/spec/fixtures/projects/{dirsproj_wildcards => file_updater_dirsproj_wildcards}/Proj2/Proj2.csproj (100%) rename nuget/spec/fixtures/projects/{dirsproj_wildcards => file_updater_dirsproj_wildcards}/Proj2/dirs.proj (100%) rename nuget/spec/fixtures/projects/{dirsproj_wildcards => file_updater_dirsproj_wildcards}/dirs.proj (100%) diff --git a/common/spec/spec_helper.rb b/common/spec/spec_helper.rb index daa77978dd6..5af810ea7b6 100644 --- a/common/spec/spec_helper.rb +++ b/common/spec/spec_helper.rb @@ -85,6 +85,32 @@ def fixture(*name) File.read(File.join("spec", "fixtures", File.join(*name))) end +# Creates a temporary directory and writes the provided files into it. +# +# @param files [DependencyFile] the files to be written into the temporary directory +def write_tmp_repo(files, + tmp_dir_path: Dependabot::Utils::BUMP_TMP_DIR_PATH, + tmp_dir_prefix: Dependabot::Utils::BUMP_TMP_FILE_PREFIX) + FileUtils.mkdir_p(tmp_dir_path) + tmp_repo = Dir.mktmpdir(tmp_dir_prefix, tmp_dir_path) + tmp_repo_path = Pathname.new(tmp_repo).expand_path + FileUtils.mkpath(tmp_repo_path) + + files.each do |file| + path = tmp_repo_path.join(file.name) + FileUtils.mkpath(path.dirname) + File.write(path, file.content) + end + + Dir.chdir(tmp_repo_path) do + Dependabot::SharedHelpers.run_shell_command("git init") + Dependabot::SharedHelpers.run_shell_command("git add --all") + Dependabot::SharedHelpers.run_shell_command("git commit -m init") + end + + tmp_repo_path.to_s +end + # Creates a temporary directory and copies in any files from the specified # project path. The project path will typically contain a dependency file and a # lockfile, but it may also include a vendor directory. A git repo will be diff --git a/nuget/spec/dependabot/nuget/file_updater_spec.rb b/nuget/spec/dependabot/nuget/file_updater_spec.rb index d74a77c805f..bfe2b04910f 100644 --- a/nuget/spec/dependabot/nuget/file_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/file_updater_spec.rb @@ -3,7 +3,9 @@ require "spec_helper" require "dependabot/source" +require "dependabot/nuget/file_parser" require "dependabot/nuget/file_updater" +require "dependabot/nuget/version" require_relative "github_helpers" require_relative "nuget_search_stubs" require "json" @@ -17,6 +19,9 @@ it_behaves_like "a dependency file updater" let(:file_updater_instance) do + Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path).parse described_class.new( dependency_files: dependency_files, dependencies: dependencies, @@ -27,8 +32,15 @@ repo_contents_path: repo_contents_path ) end + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) + end let(:dependencies) { [dependency] } - let(:project_name) { "dirsproj" } + let(:project_name) { "file_updater_dirsproj" } let(:directory) { "/" } # project_dependency files comes back with directory files first, we need the closest project at the top let(:dependency_files) { nuget_project_dependency_files(project_name, directory: directory).reverse } @@ -51,11 +63,9 @@ let(:previous_requirements) do [{ file: "dirs.proj", requirement: "1.0.0", groups: [], source: nil }] end - let(:tmp_path) { Dependabot::Utils::BUMP_TMP_DIR_PATH } let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } before do - FileUtils.mkdir_p(tmp_path) stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.0", "1.1.1"]) stub_request(:get, "https://api.nuget.org/v3-flatcontainer/" \ "microsoft.extensions.dependencymodel/1.0.0/" \ @@ -75,7 +85,7 @@ expect(file_updater_instance.send(:testonly_update_tooling_calls)).to eq( { - "#{repo_contents_path}/dirs.projMicrosoft.Extensions.DependencyModel" => 1 + "#{repo_contents_path}/Proj1/Proj1/Proj1.csprojMicrosoft.Extensions.DependencyModel" => 1 } ) end @@ -99,7 +109,7 @@ describe "#updated_dependency_files_with_wildcard" do subject(:updated_files) { file_updater_instance.updated_dependency_files } - let(:project_name) { "dirsproj_wildcards" } + let(:project_name) { "file_updater_dirsproj_wildcards" } let(:dependency_files) { nuget_project_dependency_files(project_name, directory: directory).reverse } let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } let(:dependency_version) { "1.1.1" } @@ -113,7 +123,7 @@ expect(file_updater_instance.send(:testonly_update_tooling_calls)).to eq( { - "#{repo_contents_path}/dirs.projMicrosoft.Extensions.DependencyModel" => 1, + "#{repo_contents_path}/Proj1/Proj1/Proj1.csprojMicrosoft.Extensions.DependencyModel" => 1, "#{repo_contents_path}/Proj2/Proj2.csprojMicrosoft.Extensions.DependencyModel" => 1 } ) diff --git a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb index ad6915841f7..55d5ac05e42 100644 --- a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb @@ -3,18 +3,29 @@ require "spec_helper" require "dependabot/dependency" +require "dependabot/nuget/file_parser" require "dependabot/nuget/update_checker/compatibility_checker" require "dependabot/nuget/update_checker/repository_finder" require "dependabot/nuget/update_checker/tfm_finder" RSpec.describe Dependabot::Nuget::CompatibilityChecker do subject(:checker) do + Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path).parse described_class.new( dependency_urls: dependency_urls, dependency: dependency ) end - + let(:repo_contents_path) { write_tmp_repo(dependency_files) } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) + end let(:dependency_urls) do Dependabot::Nuget::RepositoryFinder.new( dependency: dependency, diff --git a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb index 54ddc9d02c6..c3e0ba00665 100644 --- a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb @@ -4,8 +4,8 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/tfm_finder" require "dependabot/nuget/file_parser" +require "dependabot/nuget/update_checker/tfm_finder" RSpec.describe Dependabot::Nuget::TfmFinder do let(:project_name) { "tfm_finder" } diff --git a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb index 33fc544051d..912cc102fd4 100644 --- a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb @@ -4,11 +4,23 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" +require "dependabot/nuget/file_parser" require "dependabot/nuget/update_checker/version_finder" require "dependabot/nuget/update_checker/tfm_comparer" RSpec.describe Dependabot::Nuget::UpdateChecker::VersionFinder do + let(:repo_contents_path) { write_tmp_repo(dependency_files) } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) + end let(:finder) do + Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path).parse described_class.new( dependency: dependency, dependency_files: dependency_files, @@ -16,7 +28,7 @@ ignored_versions: ignored_versions, raise_on_ignored: raise_on_ignored, security_advisories: security_advisories, - repo_contents_path: "test/repo" + repo_contents_path: repo_contents_path ) end diff --git a/nuget/spec/fixtures/projects/dirsproj/Proj1/Proj1/Proj1.csproj b/nuget/spec/fixtures/projects/file_updater_dirsproj/Proj1/Proj1/Proj1.csproj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj/Proj1/Proj1/Proj1.csproj rename to nuget/spec/fixtures/projects/file_updater_dirsproj/Proj1/Proj1/Proj1.csproj diff --git a/nuget/spec/fixtures/projects/dirsproj/Proj1/dirs.proj b/nuget/spec/fixtures/projects/file_updater_dirsproj/Proj1/dirs.proj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj/Proj1/dirs.proj rename to nuget/spec/fixtures/projects/file_updater_dirsproj/Proj1/dirs.proj diff --git a/nuget/spec/fixtures/projects/dirsproj/Proj2/dirs.proj b/nuget/spec/fixtures/projects/file_updater_dirsproj/Proj2/dirs.proj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj/Proj2/dirs.proj rename to nuget/spec/fixtures/projects/file_updater_dirsproj/Proj2/dirs.proj diff --git a/nuget/spec/fixtures/projects/dirsproj/dirs.proj b/nuget/spec/fixtures/projects/file_updater_dirsproj/dirs.proj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj/dirs.proj rename to nuget/spec/fixtures/projects/file_updater_dirsproj/dirs.proj diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/Proj1/Proj1.csproj b/nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/Proj1/Proj1/Proj1.csproj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/Proj1/Proj1.csproj rename to nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/Proj1/Proj1/Proj1.csproj diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/dirs.proj b/nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/Proj1/dirs.proj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/dirs.proj rename to nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/Proj1/dirs.proj diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/Proj2.csproj b/nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/Proj2/Proj2.csproj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/Proj2.csproj rename to nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/Proj2/Proj2.csproj diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/dirs.proj b/nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/Proj2/dirs.proj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/dirs.proj rename to nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/Proj2/dirs.proj diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/dirs.proj b/nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/dirs.proj similarity index 100% rename from nuget/spec/fixtures/projects/dirsproj_wildcards/dirs.proj rename to nuget/spec/fixtures/projects/file_updater_dirsproj_wildcards/dirs.proj From df9e1fa1da67912b14ba8a3792786f23c27a55d8 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sat, 23 Mar 2024 09:47:00 -0700 Subject: [PATCH 18/26] Remove test code from discovery_json_reader --- .../nuget/discovery/discovery_json_reader.rb | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index 3db6a8462fe..e6a387558ce 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -13,11 +13,6 @@ class DiscoveryJsonReader DISCOVERY_JSON_PATH = ".dependabot/discovery.json" - sig { returns(T::Boolean) } - private_class_method def self.test_run? - ENV["DEPENDABOT_NUGET_TEST_RUN"] == "true" - end - sig { returns(String) } private_class_method def self.temp_directory Dir.tmpdir @@ -28,17 +23,8 @@ def self.discovery_file_path File.join(temp_directory, DISCOVERY_JSON_PATH) end - sig { params(discovery_json: DependencyFile).void } - def self.write_discovery_json(discovery_json) - throw "Not a test run!" unless test_run? - - @test_discovery_json = T.let(discovery_json, T.nilable(DependencyFile)) - end - sig { returns(T.nilable(DependencyFile)) } def self.discovery_json - # return @test_discovery_json if test_run? - return unless File.exist?(discovery_file_path) DependencyFile.new( From 6c48a7fdabe2c0e140653b67db36133c77614483 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sat, 23 Mar 2024 09:50:23 -0700 Subject: [PATCH 19/26] Fix sorbet type info in compatibility_checker --- .../dependabot/nuget/update_checker/compatibility_checker.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb b/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb index 8a4ce522e14..4f67de874e3 100644 --- a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb @@ -77,9 +77,7 @@ def parse_package_tfms(nuspec_xml) sig { returns(T.nilable(T::Array[String])) } def project_tfms - return @project_tfms if defined?(@project_tfms) - - @project_tfms = TfmFinder.frameworks(dependency) + @project_tfms ||= T.let(TfmFinder.frameworks(dependency), T.nilable(T::Array[String])) end sig { params(dependency_version: String).returns(T.nilable(T::Array[String])) } From 172b4fa3e6c69c4a776584647fa08df2f93edb46 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sat, 23 Mar 2024 12:27:20 -0700 Subject: [PATCH 20/26] Fix filename capitalization --- .../Discover/DiscoveryWorkerTests.Project.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs index 1ebfa677b82..492c89d38a2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs @@ -94,7 +94,7 @@ await TestDiscoveryAsync( """), - ("directory.packages.props", """ + ("Directory.Packages.props", """ true @@ -189,7 +189,7 @@ await TestDiscoveryAsync( """), - ("packages.props", """ + ("Packages.props", """ From 32c9817ef994a76366d5b0d48c984f189e004e1c Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sat, 23 Mar 2024 17:04:18 -0700 Subject: [PATCH 21/26] Fix logging test --- .../nuget/discovery/discovery_json_reader.rb | 3 +- .../spec/dependabot/nuget/file_parser_spec.rb | 84 ++++++++++++++----- .../file_parser_csproj_property/my.csproj | 9 ++ 3 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 nuget/spec/fixtures/projects/file_parser_csproj_property/my.csproj diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index e6a387558ce..b1926fe590c 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -65,8 +65,7 @@ def workspace_discovery @workspace_discovery ||= T.let(begin return nil unless discovery_json.content - puts "Discovery JSON content:" - puts discovery_json.content + Dependabot.logger.info("Discovery JSON content: #{discovery_json.content}") parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) WorkspaceDiscovery.from_json(parsed_json) diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index ef0e0f14789..3406d406f83 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -498,23 +498,7 @@ def dependencies_from_info(deps_info) end context "discovered dependencies are reported" do - let(:csproj_file) do - Dependabot::DependencyFile.new( - name: "my.csproj", - content: - <<~XML - - - net8.0 - 1.2.3 - - - - - - XML - ) - end + let(:project_name) { "file_parser_csproj_property" } before do allow(Dependabot.logger).to receive(:info) @@ -542,9 +526,69 @@ def dependencies_from_info(deps_info) expect(dependencies.length).to eq(1) # this line is really just to force evaluation so we can see the infos expect(Dependabot.logger).to have_received(:info).with( <<~INFO - The following dependencies were found: - name: Some.Package, version: 1.2.3 - file: my.csproj, metadata: {:property_name=>"SomePackageVersion"} + Discovery JSON content: { + "FilePath": "", + "IsSuccess": true, + "Projects": [ + { + "FilePath": "my.csproj", + "Dependencies": [ + { + "Name": "Microsoft.NET.Sdk", + "Version": null, + "Type": "MSBuildSdk", + "EvaluationResult": null, + "TargetFrameworks": null, + "IsDevDependency": false, + "IsDirect": false, + "IsTransitive": false, + "IsOverride": false, + "IsUpdate": false + }, + { + "Name": "Some.Package", + "Version": "1.2.3", + "Type": "PackageReference", + "EvaluationResult": { + "ResultType": "Success", + "OriginalValue": "$(SomePackageVersion)", + "EvaluatedValue": "1.2.3", + "RootPropertyName": "SomePackageVersion", + "ErrorMessage": null + }, + "TargetFrameworks": [ + "net8.0" + ], + "IsDevDependency": false, + "IsDirect": true, + "IsTransitive": false, + "IsOverride": false, + "IsUpdate": false + } + ], + "IsSuccess": true, + "Properties": [ + { + "Name": "SomePackageVersion", + "Value": "1.2.3", + "SourceFilePath": "my.csproj" + }, + { + "Name": "TargetFramework", + "Value": "net8.0", + "SourceFilePath": "my.csproj" + } + ], + "TargetFrameworks": [ + "net8.0" + ], + "ReferencedProjectPaths": [] + } + ], + "DirectoryPackagesProps": null, + "GlobalJson": null, + "DotNetToolsJson": null + } INFO .chomp ) diff --git a/nuget/spec/fixtures/projects/file_parser_csproj_property/my.csproj b/nuget/spec/fixtures/projects/file_parser_csproj_property/my.csproj new file mode 100644 index 00000000000..c90e26df55b --- /dev/null +++ b/nuget/spec/fixtures/projects/file_parser_csproj_property/my.csproj @@ -0,0 +1,9 @@ + + + net8.0 + 1.2.3 + + + + + From 5bd0745ada66d8e5ca691f8edecec166f4a71783 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 1 Apr 2024 22:03:13 -0700 Subject: [PATCH 22/26] Ensure we are including indirect dependencies from props and targets files --- .../Discover/DiscoveryWorkerTests.Project.cs | 86 ++++++++++++++++++- .../Utilities/MSBuildHelperTests.cs | 22 ++--- .../Discover/DiscoveryWorker.cs | 1 - .../Discover/SdkProjectDiscovery.cs | 12 ++- .../NuGetUpdater.Core/Utilities/JsonHelper.cs | 1 - .../Utilities/MSBuildHelper.cs | 15 ++-- 6 files changed, 112 insertions(+), 25 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs index 492c89d38a2..d4ddeeba1f1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs @@ -146,6 +146,90 @@ await TestDiscoveryAsync( }); } + [Fact] + public async Task WithDirectoryBuildPropsAndTargets() + { + await TestDiscoveryAsync( + workspacePath: "", + files: [ + ("project.csproj", """ + + + + Exe + net6.0 + enable + enable + + + + """), + ("Directory.Build.props", """ + + + + + + + + """), + ("Directory.Build.targets", """ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + """), + ], + expectedResult: new() + { + FilePath = "", + Projects = [ + new() + { + FilePath = "project.csproj", + ExpectedDependencyCount = 3, + Dependencies = [ + new("NuGet.Versioning", "6.1.0", DependencyType.PackageReference, TargetFrameworks: ["net6.0"], IsDirect: false), + new("Microsoft.CodeAnalysis.Analyzers", "3.3.0", DependencyType.PackageReference, TargetFrameworks: ["net6.0"], IsDirect: false), + ], + Properties = [ + new("ImplicitUsings", "enable", "project.csproj"), + new("Nullable", "enable", "project.csproj"), + new("OutputType", "Exe", "project.csproj"), + new("TargetFramework", "net6.0", "project.csproj"), + ], + TargetFrameworks = ["net6.0"], + }, + new() + { + FilePath = "Directory.Build.props", + ExpectedDependencyCount = 1, + Dependencies = [ + new("NuGet.Versioning", "6.1.0", DependencyType.PackageReference, IsDirect: true), + ], + Properties = [], + TargetFrameworks = [], + }, + new() + { + FilePath = "Directory.Build.targets", + ExpectedDependencyCount = 1, + Dependencies = [ + new("Microsoft.CodeAnalysis.Analyzers", "3.3.0", DependencyType.PackageReference, IsDirect: true), + ], + Properties = [], + TargetFrameworks = [], + }, + ], + }); + } + [Fact] public async Task WithPackagesProps() { @@ -215,7 +299,7 @@ await TestDiscoveryAsync( new() { FilePath = "myproj.csproj", - ExpectedDependencyCount = 52, + ExpectedDependencyCount = 58, Dependencies = [ new("Microsoft.Extensions.DependencyModel", "1.1.1", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), new("Microsoft.AspNetCore.App", "", DependencyType.PackageReference, TargetFrameworks: ["net462", "netstandard1.6"], IsDirect: true), diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs index 96de0f1e921..5b804f733fc 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -515,7 +515,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Newtonsoft.Json", "12.0.1", - DependencyType.Unknown, + DependencyType.PackageReference, EvaluationResult: new(EvaluationResultType.Success, "12.0.1", "12.0.1", null, null)) } ]; @@ -542,7 +542,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Newtonsoft.Json", "12.0.1", - DependencyType.Unknown, + DependencyType.PackageReference, EvaluationResult: new(EvaluationResultType.Success, "12.0.1", "12.0.1", null, null)) } ]; @@ -570,7 +570,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Newtonsoft.Json", "12.0.1", - DependencyType.Unknown, + DependencyType.PackageReference, new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } ]; @@ -600,7 +600,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Newtonsoft.Json", "12.0.1", - DependencyType.Unknown, + DependencyType.PackageReference, new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } ]; @@ -630,7 +630,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Newtonsoft.Json", "12.0.1", - DependencyType.Unknown, + DependencyType.PackageReference, new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } }; @@ -660,7 +660,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Newtonsoft.Json", "12.0.1", - DependencyType.Unknown, + DependencyType.PackageReference, new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } ]; @@ -690,7 +690,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Newtonsoft.Json", "12.0.1", - DependencyType.Unknown, + DependencyType.PackageReference, new(EvaluationResultType.Success, "$(NewtonsoftJsonVersion)", "12.0.1", "NewtonsoftJsonVersion", null)) } }; @@ -726,12 +726,12 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Azure.Identity", "1.6.0", - DependencyType.Unknown, + DependencyType.PackageReference, EvaluationResult: new(EvaluationResultType.Success, "1.6.0", "1.6.0", null, null)), new( "Microsoft.Data.SqlClient", "5.1.4", - DependencyType.Unknown, + DependencyType.PackageReference, EvaluationResult: new(EvaluationResultType.Success, "5.1.4", "5.1.4", null, null), IsUpdate: true), } @@ -768,12 +768,12 @@ public static IEnumerable GetTopLevelPackageDependencyInfosTestData() new( "Azure.Identity", "1.6.0", - DependencyType.Unknown, + DependencyType.PackageReference, EvaluationResult: new(EvaluationResultType.Success, "1.6.0", "1.6.0", null, null)), new( "Microsoft.Data.SqlClient", "5.1.4", - DependencyType.Unknown, + DependencyType.PackageReference, EvaluationResult: new(EvaluationResultType.Success, "5.1.4", "5.1.4", null, null), IsUpdate: true), } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index 2b855d595cd..cf27a972096 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -4,7 +4,6 @@ using NuGetUpdater.Core.Utilities; - namespace NuGetUpdater.Core.Discover; public partial class DiscoveryWorker diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs index e495a8bdc2b..761b09da503 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs @@ -31,6 +31,9 @@ public static async Task> DiscoverAsync(s var sdkDependencies = fileDependencies.Values .Where(d => d.Type == DependencyType.MSBuildSdk) .ToImmutableArray(); + var indirectDependencies = topLevelDependencies + .Where(d => !fileDependencies.ContainsKey(d.Name)) + .ToImmutableArray(); var directDependencies = topLevelDependencies .Where(d => fileDependencies.ContainsKey(d.Name)) .Select(d => @@ -59,11 +62,12 @@ public static async Task> DiscoverAsync(s .ToImmutableArray(); // Get the complete set of dependencies including transitive dependencies. - directDependencies = directDependencies + var dependencies = indirectDependencies.Concat(directDependencies).ToImmutableArray(); + dependencies = dependencies .Select(d => d with { TargetFrameworks = tfms }) .ToImmutableArray(); - var transitiveDependencies = await GetTransitiveDependencies(repoRootPath, projectPath, tfms, directDependencies, logger); - ImmutableArray dependencies = directDependencies.Concat(transitiveDependencies).Concat(sdkDependencies) + var transitiveDependencies = await GetTransitiveDependencies(repoRootPath, projectPath, tfms, dependencies, logger); + ImmutableArray allDependencies = dependencies.Concat(transitiveDependencies).Concat(sdkDependencies) .OrderBy(d => d.Name) .ToImmutableArray(); @@ -73,7 +77,7 @@ public static async Task> DiscoverAsync(s Properties = properties, TargetFrameworks = tfms, ReferencedProjectPaths = referencedProjectPaths, - Dependencies = dependencies, + Dependencies = allDependencies, }); } else diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs index 1c5683ddd6c..4e9527a2f1a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs @@ -155,7 +155,6 @@ public static string UpdateJsonProperty(string json, string[] propertyPath, stri } } - resultJson = string.Join('\n', updatedJsonLines); // the JSON writer doesn't properly maintain newlines, so we need to normalize everything diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 3110397b3fc..cb5858ba283 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -221,7 +221,7 @@ public static IReadOnlyDictionary GetProperties(ImmutableArray public static IEnumerable GetTopLevelPackageDependencyInfos(ImmutableArray buildFiles) { - Dictionary packageInfo = new(StringComparer.OrdinalIgnoreCase); + Dictionary packageInfo = new(StringComparer.OrdinalIgnoreCase); Dictionary packageVersionInfo = new(StringComparer.OrdinalIgnoreCase); Dictionary propertyInfo = new(StringComparer.OrdinalIgnoreCase); @@ -243,7 +243,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl } } - if (buildFile.RelativePath.StartsWith("..")) + if (buildFile.IsOutsideBasePath) { continue; } @@ -251,6 +251,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl foreach (var packageItem in projectRoot.Items .Where(i => (i.ItemType == "PackageReference" || i.ItemType == "GlobalPackageReference"))) { + var dependencyType = packageItem.ItemType == "PackageReference" ? DependencyType.PackageReference : DependencyType.GlobalPackageReference; var versionSpecification = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value ?? packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase))?.Value ?? string.Empty; @@ -267,12 +268,12 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl var vSpec = string.IsNullOrEmpty(versionSpecification) || existingUpdate ? existingVersion : versionSpecification; var isUpdate = existingUpdate && string.IsNullOrEmpty(packageItem.Include); - packageInfo[attributeValue] = (vSpec, isUpdate); + packageInfo[attributeValue] = (vSpec, isUpdate, dependencyType); } else { var isUpdate = !string.IsNullOrEmpty(packageItem.Update); - packageInfo[attributeValue] = (versionSpecification, isUpdate); + packageInfo[attributeValue] = (versionSpecification, isUpdate, dependencyType); } } } @@ -288,7 +289,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl foreach (var (name, info) in packageInfo) { - var (version, isUpdate) = info; + var (version, isUpdate, dependencyType) = info; if (version.Length != 0 || !packageVersionInfo.TryGetValue(name, out var packageVersion)) { packageVersion = version; @@ -303,8 +304,8 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl // We don't know the version for range requirements or wildcard // requirements, so return "" for these. yield return packageVersion.Contains(',') || packageVersion.Contains('*') - ? new Dependency(name, string.Empty, DependencyType.Unknown, EvaluationResult: evaluationResult, IsUpdate: isUpdate) - : new Dependency(name, packageVersion, DependencyType.Unknown, EvaluationResult: evaluationResult, IsUpdate: isUpdate); + ? new Dependency(name, string.Empty, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate) + : new Dependency(name, packageVersion, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate); } } From c550413a3b3d8ba0cfcf3ded0a65b1b6ac14110c Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 1 Apr 2024 22:48:56 -0700 Subject: [PATCH 23/26] Fix up reported requirements in tests --- .../nuget/discovery/dependency_file_discovery.rb | 3 +++ .../nuget/discovery/discovery_json_reader.rb | 1 + nuget/spec/dependabot/nuget/file_parser_spec.rb | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb index 0385c11c9a1..db4c8ddfc28 100644 --- a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb @@ -59,6 +59,9 @@ def dependency_set # rubocop:disable Metrics/PerceivedComplexity,Metrics/Cycloma next if dependency.name.include?("%(") || dependency.version&.include?("%(") + # Exclude any dependencies which reference an item type + next if dependency.name.include?("@(") + dependency_file_name = file_name if dependency.type == "PackagesConfig" dir_name = File.dirname(file_name) diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index b1926fe590c..805d0281744 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -66,6 +66,7 @@ def workspace_discovery return nil unless discovery_json.content Dependabot.logger.info("Discovery JSON content: #{discovery_json.content}") + puts "Discovery JSON content: #{discovery_json.content}" parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) WorkspaceDiscovery.from_json(parsed_json) diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index add69df4f22..2ae0ef504dc 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -368,6 +368,11 @@ file: "commonprops.props", groups: ["dependencies"], source: nil + }, { + requirement: "2.3.0", + file: "my.csproj", + groups: ["dependencies"], + source: nil }] ) end @@ -413,6 +418,11 @@ expect(dependency.version).to eq("1.1.1") expect(dependency.requirements).to eq( [{ + requirement: "1.1.1", + file: "my.csproj", + groups: ["dependencies"], + source: nil + }, { requirement: "1.1.1", file: "packages.props", groups: ["dependencies"], From f45dc788905408f690b57873ac455a72f494109d Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 1 Apr 2024 22:15:09 -0700 Subject: [PATCH 24/26] TEMP: Fetch modified smoke tests --- .github/workflows/smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index cddf89fc2b7..a45f6f49315 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -44,7 +44,7 @@ jobs: cat filtered.json # Curl the smoke-test tests directory to get a list of tests to run - URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests?ref=jorobich/update-nuget-tests curl $URL > tests.json # Select the names that match smoke-$test*.yaml, where $test is the .text value from filtered.json @@ -84,7 +84,7 @@ jobs: - name: Download test if: steps.cache-smoke-test.outputs.cache-hit != 'true' run: | - URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }} + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }}?ref=jorobich/update-nuget-tests curl $(gh api $URL --jq .download_url) -o smoke.yaml - name: Cache Smoke Test From abd85c728cd05a7dfef3a2479c4716b0bee92252 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Tue, 2 Apr 2024 15:33:42 -0700 Subject: [PATCH 25/26] Fix tests --- .../NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs | 2 +- nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index cb5858ba283..bb01e30d851 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -446,7 +446,7 @@ internal static async Task CreateTempProjectAsync( Environment.NewLine, packages // empty `Version` attributes will cause the temporary project to not build - .Where(p => !string.IsNullOrWhiteSpace(p.Version)) + .Where(p => (p.EvaluationResult is null || p.EvaluationResult.ResultType == EvaluationResultType.Success) && !string.IsNullOrWhiteSpace(p.Version)) // If all PackageReferences for a package are update-only mark it as such, otherwise it can cause package incoherence errors which do not exist in the repo. .Select(p => $"<{(usePackageDownload ? "PackageDownload" : "PackageReference")} {(p.IsUpdate ? "Update" : "Include")}=\"{p.Name}\" Version=\"[{p.Version}]\" />")); diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index 805d0281744..b1926fe590c 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -66,7 +66,6 @@ def workspace_discovery return nil unless discovery_json.content Dependabot.logger.info("Discovery JSON content: #{discovery_json.content}") - puts "Discovery JSON content: #{discovery_json.content}" parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) WorkspaceDiscovery.from_json(parsed_json) From 05992c0f7d1ad73057a86ed03fba402ea0ae5fa5 Mon Sep 17 00:00:00 2001 From: Nish Sinha Date: Wed, 10 Apr 2024 14:00:36 -0400 Subject: [PATCH 26/26] point smoke-test branch back at main --- .github/workflows/smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index a45f6f49315..cddf89fc2b7 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -44,7 +44,7 @@ jobs: cat filtered.json # Curl the smoke-test tests directory to get a list of tests to run - URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests?ref=jorobich/update-nuget-tests + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests curl $URL > tests.json # Select the names that match smoke-$test*.yaml, where $test is the .text value from filtered.json @@ -84,7 +84,7 @@ jobs: - name: Download test if: steps.cache-smoke-test.outputs.cache-hit != 'true' run: | - URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }}?ref=jorobich/update-nuget-tests + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }} curl $(gh api $URL --jq .download_url) -o smoke.yaml - name: Cache Smoke Test