dependabot-nuget 0.240.0 → 0.241.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/build +45 -0
  3. data/helpers/lib/NuGetUpdater/.editorconfig +364 -0
  4. data/helpers/lib/NuGetUpdater/.gitignore +5 -0
  5. data/helpers/lib/NuGetUpdater/Directory.Build.props +10 -0
  6. data/helpers/lib/NuGetUpdater/Directory.Common.props +16 -0
  7. data/helpers/lib/NuGetUpdater/NuGetProjects/Directory.Build.props +14 -0
  8. data/helpers/lib/NuGetUpdater/NuGetProjects/Directory.Build.targets +7 -0
  9. data/helpers/lib/NuGetUpdater/NuGetProjects/Directory.Packages.props +29 -0
  10. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Build.Tasks/NuGet.Build.Tasks.csproj +27 -0
  11. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/AssemblyMetadataExtractor.cs +203 -0
  12. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/NuGet.CommandLine.csproj +33 -0
  13. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Commands/NuGet.Commands.csproj +26 -0
  14. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Common/NuGet.Common.csproj +21 -0
  15. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Config +6 -0
  16. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Configuration/NuGet.Configuration.csproj +24 -0
  17. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Credentials/NuGet.Credentials.csproj +20 -0
  18. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.DependencyResolver.Core/NuGet.DependencyResolver.Core.csproj +22 -0
  19. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Frameworks/NuGet.Frameworks.csproj +17 -0
  20. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.LibraryModel/NuGet.LibraryModel.csproj +17 -0
  21. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.PackageManagement/NuGet.PackageManagement.csproj +27 -0
  22. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Packaging/NuGet.Packaging.csproj +28 -0
  23. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.ProjectModel/NuGet.ProjectModel.csproj +20 -0
  24. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Protocol/NuGet.Protocol.csproj +21 -0
  25. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Resolver/NuGet.Resolver.csproj +20 -0
  26. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Versioning/NuGet.Versioning.csproj +17 -0
  27. data/helpers/lib/NuGetUpdater/NuGetProjects/README.md +1 -0
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/FrameworkCheckCommand.cs +35 -0
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +43 -0
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/NuGetUpdater.Cli.csproj +20 -0
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +31 -0
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs +42 -0
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +323 -0
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/NuGetUpdater.Cli.Test.csproj +24 -0
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +3 -0
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyType.cs +12 -0
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs +97 -0
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/DotNetToolsJsonBuildFile.cs +24 -0
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs +25 -0
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs +32 -0
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +31 -0
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +94 -0
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/XmlBuildFile.cs +14 -0
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +39 -0
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +73 -0
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +146 -0
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +27 -0
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +316 -0
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +87 -0
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/ConfigurationFile.cs +3 -0
  51. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +66 -0
  52. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +48 -0
  53. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +172 -0
  54. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +498 -0
  55. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateResult.cs +7 -0
  56. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +105 -0
  57. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +222 -0
  58. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs +24 -0
  59. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +443 -0
  60. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +15 -0
  61. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +69 -0
  62. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +66 -0
  63. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +124 -0
  64. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/DotNetToolsJsonBuildFileTests.cs +52 -0
  65. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs +63 -0
  66. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs +63 -0
  67. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs +154 -0
  68. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/CompatibilityCheckerFacts.cs +64 -0
  69. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/FrameworkCompatibilityServiceFacts.cs +122 -0
  70. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs +68 -0
  71. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj +23 -0
  72. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs +36 -0
  73. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestExtensions.cs +15 -0
  74. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs +79 -0
  75. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorker.DirsProj.cs +201 -0
  76. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +147 -0
  77. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs +225 -0
  78. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs +217 -0
  79. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +94 -0
  80. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +938 -0
  81. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +2177 -0
  82. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/JsonHelperTests.cs +239 -0
  83. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +394 -0
  84. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs +179 -0
  85. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterTests.cs +238 -0
  86. data/helpers/lib/NuGetUpdater/NuGetUpdater.sln +152 -0
  87. data/helpers/lib/NuGetUpdater/xunit.runner.json +4 -0
  88. data/lib/dependabot/nuget/metadata_finder.rb +4 -4
  89. metadata +91 -5
@@ -0,0 +1,222 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.IO;
4
+ using System.Linq;
5
+ using System.Runtime.InteropServices;
6
+ using System.Text;
7
+ using System.Text.Json;
8
+ using System.Text.Json.Nodes;
9
+
10
+ namespace NuGetUpdater.Core.Utilities
11
+ {
12
+ internal static class JsonHelper
13
+ {
14
+ public static JsonDocumentOptions DocumentOptions { get; } = new JsonDocumentOptions()
15
+ {
16
+ CommentHandling = JsonCommentHandling.Skip,
17
+ };
18
+
19
+ public static JsonNode? ParseNode(string content)
20
+ {
21
+ var node = JsonNode.Parse(content, documentOptions: DocumentOptions);
22
+ return node;
23
+ }
24
+
25
+ public static string UpdateJsonProperty(string json, string[] propertyPath, string newValue, StringComparison comparisonType = StringComparison.Ordinal)
26
+ {
27
+ var readerOptions = new JsonReaderOptions()
28
+ {
29
+ CommentHandling = JsonCommentHandling.Allow,
30
+ };
31
+ var bytes = Encoding.UTF8.GetBytes(json);
32
+ var reader = new Utf8JsonReader(bytes, readerOptions);
33
+ using var ms = new MemoryStream();
34
+ var writerOptions = new JsonWriterOptions()
35
+ {
36
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
37
+ Indented = true,
38
+ };
39
+ var writer = new Utf8JsonWriter(ms, writerOptions);
40
+
41
+ var pathDepth = -1;
42
+ var currentPath = new List<string>();
43
+ var replaceNextToken = false;
44
+ while (reader.Read())
45
+ {
46
+ if (replaceNextToken)
47
+ {
48
+ // replace this specific token
49
+ writer.WriteStringValue(newValue);
50
+ replaceNextToken = false;
51
+ }
52
+ else
53
+ {
54
+ // just mirror the token
55
+ switch (reader.TokenType)
56
+ {
57
+ case JsonTokenType.Comment:
58
+ var commentValue = reader.GetComment();
59
+ var commentStart = json.Substring((int)reader.TokenStartIndex, 2);
60
+ if (commentStart == "//")
61
+ {
62
+ // Utf8JsonWriter only supports block comments, so we have to manually inject a single line comment when appropriate
63
+ writer.Flush();
64
+ var commentPrefix = GetCurrentTokenTriviaPrefix((int)reader.TokenStartIndex, json);
65
+ ms.Write(Encoding.UTF8.GetBytes(commentPrefix));
66
+ ms.Write(Encoding.UTF8.GetBytes("//" + commentValue));
67
+ }
68
+ else
69
+ {
70
+ // let the default block comment writer handle it
71
+ writer.WriteCommentValue(reader.GetComment());
72
+ }
73
+ break;
74
+ case JsonTokenType.EndArray:
75
+ writer.WriteEndArray();
76
+ break;
77
+ case JsonTokenType.EndObject:
78
+ writer.WriteEndObject();
79
+ pathDepth--;
80
+ break;
81
+ case JsonTokenType.False:
82
+ writer.WriteBooleanValue(false);
83
+ break;
84
+ case JsonTokenType.None:
85
+ // do nothing
86
+ break;
87
+ case JsonTokenType.Null:
88
+ writer.WriteNullValue();
89
+ break;
90
+ case JsonTokenType.Number:
91
+ writer.WriteNumberValue(reader.GetDouble());
92
+ break;
93
+ case JsonTokenType.PropertyName:
94
+ writer.WritePropertyName(reader.GetString()!);
95
+ break;
96
+ case JsonTokenType.StartArray:
97
+ writer.WriteStartArray();
98
+ break;
99
+ case JsonTokenType.StartObject:
100
+ writer.WriteStartObject();
101
+ pathDepth++;
102
+ break;
103
+ case JsonTokenType.String:
104
+ writer.WriteStringValue(reader.GetString());
105
+ break;
106
+ case JsonTokenType.True:
107
+ writer.WriteBooleanValue(true);
108
+ break;
109
+ default:
110
+ throw new NotImplementedException($"Unexpected token type: {reader.TokenType}");
111
+ }
112
+ }
113
+
114
+ // see if we need to replace the next token
115
+ if (reader.TokenType == JsonTokenType.PropertyName)
116
+ {
117
+ var pathValue = reader.GetString()!;
118
+
119
+ // ensure the current path object is of the correct size
120
+ while (currentPath.Count < pathDepth + 1)
121
+ {
122
+ currentPath.Add(string.Empty);
123
+ }
124
+
125
+ while (currentPath.Count > 0 && currentPath.Count > pathDepth + 1)
126
+ {
127
+ currentPath.RemoveAt(currentPath.Count - 1);
128
+ }
129
+
130
+ currentPath[pathDepth] = pathValue;
131
+ if (IsPathMatch(currentPath, propertyPath, comparisonType))
132
+ {
133
+ replaceNextToken = true;
134
+ }
135
+ }
136
+ }
137
+
138
+ writer.Flush();
139
+ ms.Flush();
140
+ ms.Seek(0, SeekOrigin.Begin);
141
+ var resultBytes = ms.ToArray();
142
+ var resultJson = Encoding.UTF8.GetString(resultBytes);
143
+
144
+ // single line comments might have had a trailing comma appended by the property writer that we can't
145
+ // control, so we have to manually correct for it
146
+ var originalJsonLines = json.Split('\n').Select(l => l.TrimEnd('\r')).ToArray();
147
+ var updatedJsonLines = resultJson.Split('\n').Select(l => l.TrimEnd('\r')).ToArray();
148
+ for (int i = 0; i < Math.Min(originalJsonLines.Length, updatedJsonLines.Length); i++)
149
+ {
150
+ if (updatedJsonLines[i].EndsWith(",") && !originalJsonLines[i].EndsWith(","))
151
+ {
152
+ updatedJsonLines[i] = updatedJsonLines[i][..^1];
153
+ }
154
+ }
155
+
156
+
157
+ resultJson = string.Join('\n', updatedJsonLines);
158
+
159
+ // the JSON writer doesn't properly maintain newlines, so we need to normalize everything
160
+ resultJson = resultJson.Replace("\r\n", "\n"); // CRLF => LF
161
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
162
+ {
163
+ resultJson = resultJson.Replace("\n", "\r\n"); // LF => CRLF
164
+ }
165
+
166
+ return resultJson;
167
+ }
168
+
169
+ private static bool IsPathMatch(List<string> currentPath, string[] expectedPath, StringComparison comparisonType) =>
170
+ currentPath.Count == expectedPath.Length &&
171
+ currentPath.Zip(expectedPath).All(pair => string.Compare(pair.First, pair.Second, comparisonType) == 0);
172
+
173
+ private static string GetCurrentTokenTriviaPrefix(int tokenStartIndex, string originalJson)
174
+ {
175
+ var prefixStart = tokenStartIndex - 1;
176
+ for (; prefixStart >= 0; prefixStart--)
177
+ {
178
+ var c = originalJson[prefixStart];
179
+ switch (c)
180
+ {
181
+ case ' ':
182
+ case '\t':
183
+ // just more whitespace; keep looking
184
+ break;
185
+ case '\r':
186
+ case '\n':
187
+ // quit at newline, modulo some special cases
188
+ if (c == '\n')
189
+ {
190
+ // check for preceeding CR
191
+ if (IsPreceedingCharacterEqual(originalJson, prefixStart, '\r'))
192
+ {
193
+ prefixStart--;
194
+ }
195
+ }
196
+
197
+ // check for preceeding comma
198
+ if (IsPreceedingCharacterEqual(originalJson, prefixStart, ','))
199
+ {
200
+ prefixStart--;
201
+ }
202
+ goto done;
203
+ default:
204
+ // found regular character; move forward one and quit
205
+ prefixStart++;
206
+ goto done;
207
+ }
208
+ }
209
+
210
+ done:
211
+ var prefix = originalJson.Substring(prefixStart, tokenStartIndex - prefixStart);
212
+ return prefix;
213
+ }
214
+
215
+ private static bool IsPreceedingCharacterEqual(string originalText, int currentIndex, char expectedCharacter)
216
+ {
217
+ return currentIndex > 0
218
+ && currentIndex < originalText.Length
219
+ && originalText[currentIndex - 1] == expectedCharacter;
220
+ }
221
+ }
222
+ }
@@ -0,0 +1,24 @@
1
+ using System;
2
+ using System.IO;
3
+
4
+ namespace NuGetUpdater.Core;
5
+
6
+ public sealed class Logger
7
+ {
8
+ public bool Verbose { get; set; }
9
+ private readonly TextWriter _logOutput;
10
+
11
+ public Logger(bool verbose)
12
+ {
13
+ Verbose = verbose;
14
+ _logOutput = Console.Out;
15
+ }
16
+
17
+ public void Log(string message)
18
+ {
19
+ if (Verbose)
20
+ {
21
+ _logOutput.WriteLine(message);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,443 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Collections.Immutable;
4
+ using System.Diagnostics.CodeAnalysis;
5
+ using System.IO;
6
+ using System.Linq;
7
+ using System.Text;
8
+ using System.Text.Json;
9
+ using System.Text.RegularExpressions;
10
+ using System.Threading.Tasks;
11
+ using System.Xml;
12
+
13
+ using Microsoft.Build.Construction;
14
+ using Microsoft.Build.Definition;
15
+ using Microsoft.Build.Evaluation;
16
+ using Microsoft.Build.Exceptions;
17
+ using Microsoft.Build.Locator;
18
+
19
+ using NuGetUpdater.Core.Utilities;
20
+
21
+ namespace NuGetUpdater.Core;
22
+
23
+ internal static partial class MSBuildHelper
24
+ {
25
+ public static string MSBuildPath { get; private set; } = string.Empty;
26
+
27
+ public static bool IsMSBuildRegistered => MSBuildPath.Length > 0;
28
+
29
+ static MSBuildHelper()
30
+ {
31
+ RegisterMSBuild();
32
+ }
33
+
34
+ public static void RegisterMSBuild()
35
+ {
36
+ // Ensure MSBuild types are registered before calling a method that loads the types
37
+ if (!IsMSBuildRegistered)
38
+ {
39
+ var defaultInstance = MSBuildLocator.QueryVisualStudioInstances().First();
40
+ MSBuildPath = defaultInstance.MSBuildPath;
41
+ MSBuildLocator.RegisterInstance(defaultInstance);
42
+ }
43
+ }
44
+
45
+ public static string[] GetTargetFrameworkMonikers(ImmutableArray<ProjectBuildFile> buildFiles)
46
+ {
47
+ HashSet<string> targetFrameworkValues = new(StringComparer.OrdinalIgnoreCase);
48
+ Dictionary<string, string> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
49
+
50
+ foreach (var buildFile in buildFiles)
51
+ {
52
+ var projectRoot = CreateProjectRootElement(buildFile);
53
+
54
+ foreach (var property in projectRoot.Properties)
55
+ {
56
+ if (property.Name.Equals("TargetFramework", StringComparison.OrdinalIgnoreCase) ||
57
+ property.Name.Equals("TargetFrameworks", StringComparison.OrdinalIgnoreCase))
58
+ {
59
+ targetFrameworkValues.Add(property.Value);
60
+ }
61
+ else if (property.Name.Equals("TargetFrameworkVersion", StringComparison.OrdinalIgnoreCase))
62
+ {
63
+ // For packages.config projects that use TargetFrameworkVersion, we need to convert it to TargetFramework
64
+ targetFrameworkValues.Add($"net{property.Value.TrimStart('v').Replace(".", "")}");
65
+ }
66
+ else
67
+ {
68
+ propertyInfo[property.Name] = property.Value;
69
+ }
70
+ }
71
+ }
72
+
73
+ HashSet<string> targetFrameworks = new(StringComparer.OrdinalIgnoreCase);
74
+
75
+ foreach (var targetFrameworkValue in targetFrameworkValues)
76
+ {
77
+ var tfms = targetFrameworkValue;
78
+ tfms = GetRootedValue(tfms, propertyInfo);
79
+
80
+ if (string.IsNullOrEmpty(tfms))
81
+ {
82
+ continue;
83
+ }
84
+
85
+ foreach (var tfm in tfms.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
86
+ {
87
+ targetFrameworks.Add(tfm);
88
+ }
89
+ }
90
+
91
+ return targetFrameworks.ToArray();
92
+ }
93
+
94
+ public static IEnumerable<string> GetProjectPathsFromSolution(string solutionPath)
95
+ {
96
+ var solution = SolutionFile.Parse(solutionPath);
97
+ return solution.ProjectsInOrder.Select(p => p.AbsolutePath);
98
+ }
99
+
100
+ public static IEnumerable<string> GetProjectPathsFromProject(string projFilePath)
101
+ {
102
+ var projectStack = new Stack<(string folderPath, ProjectRootElement)>();
103
+ var projectRootElement = ProjectRootElement.Open(projFilePath);
104
+
105
+ projectStack.Push((Path.GetFullPath(Path.GetDirectoryName(projFilePath)!), projectRootElement));
106
+
107
+ while (projectStack.Count > 0)
108
+ {
109
+ var (folderPath, tmpProject) = projectStack.Pop();
110
+ foreach (var projectReference in tmpProject.Items.Where(static x => x.ItemType == "ProjectReference" || x.ItemType == "ProjectFile"))
111
+ {
112
+ if (projectReference.Include is not { } projectPath)
113
+ {
114
+ continue;
115
+ }
116
+
117
+ projectPath = PathHelper.GetFullPathFromRelative(folderPath, projectPath);
118
+
119
+ var projectExtension = Path.GetExtension(projectPath).ToLowerInvariant();
120
+ if (projectExtension == ".proj")
121
+ {
122
+ // If there is some MSBuild logic that needs to run to fully resolve the path skip the project
123
+ if (File.Exists(projectPath))
124
+ {
125
+ var additionalProjectRootElement = ProjectRootElement.Open(projectPath);
126
+ projectStack.Push((Path.GetFullPath(Path.GetDirectoryName(projectPath)!), additionalProjectRootElement));
127
+ }
128
+ }
129
+ else if (projectExtension == ".csproj" || projectExtension == ".vbproj" || projectExtension == ".fsproj")
130
+ {
131
+ yield return projectPath;
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ public static IEnumerable<Dependency> GetTopLevelPackageDependenyInfos(ImmutableArray<ProjectBuildFile> buildFiles)
138
+ {
139
+ Dictionary<string, string> packageInfo = new(StringComparer.OrdinalIgnoreCase);
140
+ Dictionary<string, string> packageVersionInfo = new(StringComparer.OrdinalIgnoreCase);
141
+ Dictionary<string, string> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
142
+
143
+ foreach (var buildFile in buildFiles)
144
+ {
145
+ var projectRoot = CreateProjectRootElement(buildFile);
146
+
147
+ foreach (var packageItem in projectRoot.Items
148
+ .Where(i => (i.ItemType == "PackageReference" || i.ItemType == "GlobalPackageReference")))
149
+ {
150
+ var versionSpecification = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value
151
+ ?? packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase))?.Value
152
+ ?? string.Empty;
153
+ foreach (var attributeValue in new[] { packageItem.Include, packageItem.Update })
154
+ {
155
+ if (!string.IsNullOrWhiteSpace(attributeValue))
156
+ {
157
+ packageInfo[attributeValue] = versionSpecification;
158
+ }
159
+ }
160
+ }
161
+
162
+ foreach (var packageItem in projectRoot.Items
163
+ .Where(i => i.ItemType == "PackageVersion" && !string.IsNullOrEmpty(i.Include)))
164
+ {
165
+ packageVersionInfo[packageItem.Include] = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value
166
+ ?? string.Empty;
167
+ }
168
+
169
+ foreach (var property in projectRoot.Properties)
170
+ {
171
+ // Short of evaluating the entire project, there's no way to _really_ know what package version is
172
+ // going to be used, and even then we might not be able to update it. As a best guess, we'll simply
173
+ // skip any property that has a condition _or_ where the condition is checking for an empty string.
174
+ var hasEmptyCondition = string.IsNullOrEmpty(property.Condition);
175
+ var conditionIsCheckingForEmptyString = string.Equals(property.Condition, $"$({property.Name}) == ''", StringComparison.OrdinalIgnoreCase);
176
+ if (hasEmptyCondition || conditionIsCheckingForEmptyString)
177
+ {
178
+ propertyInfo[property.Name] = property.Value;
179
+ }
180
+ }
181
+ }
182
+
183
+ foreach (var (name, version) in packageInfo)
184
+ {
185
+ if (version.Length != 0 || !packageVersionInfo.TryGetValue(name, out var packageVersion))
186
+ {
187
+ packageVersion = version;
188
+ }
189
+
190
+ // Walk the property replacements until we don't find another one.
191
+ packageVersion = GetRootedValue(packageVersion, propertyInfo);
192
+
193
+ packageVersion = packageVersion.TrimStart('[', '(').TrimEnd(']', ')');
194
+
195
+ // We don't know the version for range requirements or wildcard
196
+ // requirements, so return "" for these.
197
+ yield return packageVersion.Contains(',') || packageVersion.Contains('*')
198
+ ? new Dependency(name, string.Empty, DependencyType.Unknown)
199
+ : new Dependency(name, packageVersion, DependencyType.Unknown);
200
+ }
201
+ }
202
+
203
+ /// <summary>
204
+ /// Given an MSBuild string and a set of properties, returns our best guess at the final value MSBuild will evaluate to.
205
+ /// </summary>
206
+ /// <param name="msbuildString"></param>
207
+ /// <param name="propertyInfo"></param>
208
+ /// <returns></returns>
209
+ public static string GetRootedValue(string msbuildString, Dictionary<string, string> propertyInfo)
210
+ {
211
+ var seenProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
212
+ while (TryGetPropertyName(msbuildString, out var propertyName))
213
+ {
214
+ if (!seenProperties.Add(propertyName))
215
+ {
216
+ throw new InvalidDataException($"Property '{propertyName}' has a circular reference.");
217
+ }
218
+
219
+ msbuildString = propertyInfo.TryGetValue(propertyName, out var propertyValue)
220
+ ? msbuildString.Replace($"$({propertyName})", propertyValue)
221
+ : throw new InvalidDataException($"Property '{propertyName}' was not found.");
222
+ }
223
+
224
+ return msbuildString;
225
+ }
226
+
227
+ public static bool TryGetPropertyName(string versionContent, [NotNullWhen(true)] out string? propertyName)
228
+ {
229
+ var startIndex = versionContent.IndexOf("$(", StringComparison.Ordinal);
230
+ if (startIndex != -1)
231
+ {
232
+ var endIndex = versionContent.IndexOf(')', startIndex);
233
+ if (endIndex != -1)
234
+ {
235
+ propertyName = versionContent.Substring(startIndex + 2, endIndex - startIndex - 2);
236
+ return true;
237
+ }
238
+ }
239
+
240
+ propertyName = null;
241
+ return false;
242
+ }
243
+
244
+ internal static async Task<bool> DependenciesAreCoherentAsync(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger logger)
245
+ {
246
+ var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
247
+ try
248
+ {
249
+ var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
250
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\"");
251
+
252
+ // NU1608: Detected package version outside of dependency constraint
253
+
254
+ return exitCode == 0 && !stdOut.Contains("NU1608");
255
+ }
256
+ finally
257
+ {
258
+ tempDirectory.Delete(recursive: true);
259
+ }
260
+ }
261
+
262
+ private static ProjectRootElement CreateProjectRootElement(ProjectBuildFile buildFile)
263
+ {
264
+ var xmlString = buildFile.Contents.ToFullString();
265
+ using var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlString));
266
+ using var xmlReader = XmlReader.Create(xmlStream);
267
+ var projectRoot = ProjectRootElement.Create(xmlReader);
268
+
269
+ return projectRoot;
270
+ }
271
+
272
+ private static async Task<string> CreateTempProjectAsync(DirectoryInfo tempDir, string repoRoot, string projectPath, string targetFramework, Dependency[] packages)
273
+ {
274
+ var projectDirectory = Path.GetDirectoryName(projectPath);
275
+ projectDirectory ??= repoRoot;
276
+ var topLevelFiles = Directory.GetFiles(repoRoot);
277
+ var nugetConfigPath = PathHelper.GetFileInDirectoryOrParent(projectDirectory, repoRoot, "NuGet.Config", caseSensitive: false);
278
+ if (nugetConfigPath is not null)
279
+ {
280
+ File.Copy(nugetConfigPath, Path.Combine(tempDir.FullName, "NuGet.Config"));
281
+ }
282
+
283
+ var packageReferences = string.Join(
284
+ Environment.NewLine,
285
+ packages
286
+ .Where(p => !string.IsNullOrWhiteSpace(p.Version)) // empty `Version` attributes will cause the temporary project to not build
287
+ .Select(static p => $"<PackageReference Include=\"{p.Name}\" Version=\"[{p.Version}]\" />"));
288
+
289
+ var projectContents = $"""
290
+ <Project Sdk="Microsoft.NET.Sdk">
291
+ <PropertyGroup>
292
+ <TargetFramework>{targetFramework}</TargetFramework>
293
+ <GenerateDependencyFile>true</GenerateDependencyFile>
294
+ </PropertyGroup>
295
+ <ItemGroup>
296
+ {packageReferences}
297
+ </ItemGroup>
298
+ <Target Name="_CollectDependencies" DependsOnTargets="GenerateBuildDependencyFile">
299
+ <ItemGroup>
300
+ <_NuGetPackageData Include="@(NativeCopyLocalItems)" />
301
+ <_NuGetPackageData Include="@(ResourceCopyLocalItems)" />
302
+ <_NuGetPackageData Include="@(RuntimeCopyLocalItems)" />
303
+ <_NuGetPackageData Include="@(ResolvedAnalyzers)" />
304
+ <_NuGetPackageData Include="@(_PackageDependenciesDesignTime)">
305
+ <NuGetPackageId>%(_PackageDependenciesDesignTime.Name)</NuGetPackageId>
306
+ <NuGetPackageVersion>%(_PackageDependenciesDesignTime.Version)</NuGetPackageVersion>
307
+ </_NuGetPackageData>
308
+ </ItemGroup>
309
+ </Target>
310
+ <Target Name="_ReportDependencies" DependsOnTargets="_CollectDependencies">
311
+ <Message Text="NuGetData::Package=%(_NuGetPackageData.NuGetPackageId), Version=%(_NuGetPackageData.NuGetPackageVersion)"
312
+ Condition="'%(_NuGetPackageData.NuGetPackageId)' != '' AND '%(_NuGetPackageData.NuGetPackageVersion)' != ''"
313
+ Importance="High" />
314
+ </Target>
315
+ </Project>
316
+ """;
317
+ var tempProjectPath = Path.Combine(tempDir.FullName, "Project.csproj");
318
+ await File.WriteAllTextAsync(tempProjectPath, projectContents);
319
+
320
+ // prevent directory crawling
321
+ await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Build.props"), "<Project />");
322
+ await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Build.targets"), "<Project />");
323
+ await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Packages.props"), "<Project />");
324
+
325
+ return tempProjectPath;
326
+ }
327
+
328
+ internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(
329
+ string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger? logger = null)
330
+ {
331
+ var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_");
332
+ try
333
+ {
334
+ var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
335
+
336
+ var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\" /t:_ReportDependencies");
337
+
338
+ if (exitCode == 0)
339
+ {
340
+ var lines = stdout.Split('\n').Select(line => line.Trim());
341
+ var pattern = PackagePattern();
342
+ var allDependencies = lines
343
+ .Select(line => pattern.Match(line))
344
+ .Where(match => match.Success)
345
+ .Select(match => new Dependency(match.Groups["PackageName"].Value, match.Groups["PackageVersion"].Value, DependencyType.Unknown))
346
+ .ToArray();
347
+
348
+ return allDependencies;
349
+ }
350
+ else
351
+ {
352
+ logger?.Log($"dotnet build in {nameof(GetAllPackageDependenciesAsync)} failed. STDOUT: {stdout} STDERR: {stderr}");
353
+ return Array.Empty<Dependency>();
354
+ }
355
+ }
356
+ finally
357
+ {
358
+ try
359
+ {
360
+ tempDirectory.Delete(recursive: true);
361
+ }
362
+ catch
363
+ {
364
+ }
365
+ }
366
+ }
367
+
368
+ internal static string? GetGlobalJsonPath(string repoRootPath, string projectPath)
369
+ {
370
+ return PathHelper.GetFileInDirectoryOrParent(Path.GetDirectoryName(projectPath)!, repoRootPath, "global.json");
371
+ }
372
+
373
+ internal static async Task<ImmutableArray<ProjectBuildFile>> LoadBuildFiles(string repoRootPath, string projectPath)
374
+ {
375
+ var buildFileList = new List<string>()
376
+ {
377
+ projectPath.NormalizePathToUnix() // always include the starting project
378
+ };
379
+
380
+ // a global.json file might cause problems with the dotnet msbuild command; create a safe version temporarily
381
+ var globalJsonPath = GetGlobalJsonPath(repoRootPath, projectPath);
382
+ var safeGlobalJsonName = $"{globalJsonPath}{Guid.NewGuid()}";
383
+
384
+ try
385
+ {
386
+ // move the original
387
+ if (globalJsonPath is not null)
388
+ {
389
+ File.Move(globalJsonPath, safeGlobalJsonName);
390
+
391
+ // create a safe version with only certain top-level keys
392
+ var globalJsonContent = await File.ReadAllTextAsync(safeGlobalJsonName);
393
+ var json = JsonHelper.ParseNode(globalJsonContent);
394
+ var sdks = json["msbuild-sdks"];
395
+ if (sdks is not null)
396
+ {
397
+ var newObject = new Dictionary<string, object>()
398
+ {
399
+ { "msbuild-sdks", sdks }
400
+ };
401
+ var newContent = JsonSerializer.Serialize(newObject);
402
+ await File.WriteAllTextAsync(globalJsonPath, newContent);
403
+ }
404
+ }
405
+
406
+ // This is equivalent to running the command `dotnet msbuild <projectPath> /pp` to preprocess the file.
407
+ // The only difference is that we're specifying the `IgnoreMissingImports` flag which will allow us to
408
+ // load the project even if it imports a file that doesn't exist (e.g. a file that's generated at restore
409
+ // or build time).
410
+ using var projectCollection = new ProjectCollection(); // do this in a one-off instance and don't pollute the global collection
411
+ var project = Project.FromFile(projectPath, new ProjectOptions()
412
+ {
413
+ LoadSettings = ProjectLoadSettings.IgnoreMissingImports,
414
+ ProjectCollection = projectCollection,
415
+ });
416
+ buildFileList.AddRange(project.Imports.Select(i => i.ImportedProject.FullPath.NormalizePathToUnix()));
417
+ }
418
+ catch (InvalidProjectFileException)
419
+ {
420
+ return [];
421
+ }
422
+ finally
423
+ {
424
+ if (globalJsonPath is not null)
425
+ {
426
+ File.Move(safeGlobalJsonName, globalJsonPath, overwrite: true);
427
+ }
428
+ }
429
+
430
+ var repoRootPathPrefix = repoRootPath.NormalizePathToUnix() + "/";
431
+ var buildFilesInRepo = buildFileList
432
+ .Where(f => f.StartsWith(repoRootPathPrefix, StringComparison.OrdinalIgnoreCase))
433
+ .Distinct()
434
+ .ToArray();
435
+ var result = buildFilesInRepo
436
+ .Select(path => ProjectBuildFile.Open(repoRootPath, path))
437
+ .ToImmutableArray();
438
+ return result;
439
+ }
440
+
441
+ [GeneratedRegex("^\\s*NuGetData::Package=(?<PackageName>[^,]+), Version=(?<PackageVersion>.+)$")]
442
+ private static partial Regex PackagePattern();
443
+ }