dependabot-nuget 0.250.0 → 0.252.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/Directory.Common.props +1 -0
  3. data/helpers/lib/NuGetUpdater/Directory.Packages.props +26 -0
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +35 -0
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/NuGetUpdater.Cli.csproj +1 -1
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +4 -7
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +251 -0
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/NuGetUpdater.Cli.Test.csproj +3 -3
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +56 -1
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyType.cs +1 -1
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscovery.cs +69 -0
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DirectoryPackagesPropsDiscoveryResult.cs +11 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +217 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscovery.cs +30 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DotNetToolsJsonDiscoveryResult.cs +10 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscovery.cs +30 -0
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/GlobalJsonDiscoveryResult.cs +10 -0
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/IDiscoveryResult.cs +14 -0
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscovery.cs +29 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/PackagesConfigDiscoveryResult.cs +10 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs +13 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +127 -0
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +13 -0
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResult.cs +8 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EvaluationResultType.cs +9 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs +6 -8
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/DotNetToolsJsonBuildFile.cs +4 -7
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs +24 -17
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs +2 -2
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +8 -13
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +100 -19
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/XmlBuildFile.cs +2 -2
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +6 -6
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Property.cs +6 -0
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +23 -36
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +5 -10
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +16 -21
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +4 -19
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs +14 -0
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ImmutableArrayExtensions.cs +18 -0
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +0 -1
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +121 -67
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +27 -4
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +117 -0
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs +91 -0
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs +71 -0
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +59 -0
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +380 -0
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +306 -0
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +36 -0
  51. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/DotNetToolsJsonBuildFileTests.cs +1 -2
  52. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs +2 -3
  53. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs +4 -6
  54. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs +6 -5
  55. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj +4 -3
  56. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs +38 -6
  57. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +12 -40
  58. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +30 -0
  59. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/AssertEx.cs +272 -0
  60. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/DiffUtil.cs +266 -0
  61. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +195 -152
  62. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs +7 -11
  63. data/lib/dependabot/nuget/discovery/dependency_details.rb +95 -0
  64. data/lib/dependabot/nuget/discovery/dependency_file_discovery.rb +126 -0
  65. data/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb +43 -0
  66. data/lib/dependabot/nuget/discovery/discovery_json_reader.rb +83 -0
  67. data/lib/dependabot/nuget/discovery/evaluation_details.rb +63 -0
  68. data/lib/dependabot/nuget/discovery/project_discovery.rb +71 -0
  69. data/lib/dependabot/nuget/discovery/property_details.rb +43 -0
  70. data/lib/dependabot/nuget/discovery/workspace_discovery.rb +66 -0
  71. data/lib/dependabot/nuget/file_parser.rb +19 -128
  72. data/lib/dependabot/nuget/file_updater.rb +28 -60
  73. data/lib/dependabot/nuget/native_helpers.rb +55 -0
  74. data/lib/dependabot/nuget/update_checker/compatibility_checker.rb +3 -8
  75. data/lib/dependabot/nuget/update_checker/dependency_finder.rb +1 -0
  76. data/lib/dependabot/nuget/update_checker/property_updater.rb +1 -0
  77. data/lib/dependabot/nuget/update_checker/tfm_finder.rb +17 -152
  78. data/lib/dependabot/nuget/update_checker/version_finder.rb +1 -6
  79. data/lib/dependabot/nuget/update_checker.rb +4 -1
  80. metadata +43 -11
  81. data/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb +0 -71
  82. data/lib/dependabot/nuget/file_parser/global_json_parser.rb +0 -68
  83. data/lib/dependabot/nuget/file_parser/packages_config_parser.rb +0 -92
  84. data/lib/dependabot/nuget/file_parser/project_file_parser.rb +0 -620
  85. data/lib/dependabot/nuget/file_parser/property_value_finder.rb +0 -225
  86. data/lib/dependabot/nuget/file_updater/property_value_updater.rb +0 -81
@@ -1,13 +1,8 @@
1
- using System;
2
- using System.Collections.Generic;
3
1
  using System.Collections.Immutable;
4
2
  using System.Diagnostics.CodeAnalysis;
5
- using System.IO;
6
- using System.Linq;
7
3
  using System.Text;
8
4
  using System.Text.Json;
9
5
  using System.Text.RegularExpressions;
10
- using System.Threading.Tasks;
11
6
  using System.Xml;
12
7
 
13
8
  using Microsoft.Build.Construction;
@@ -23,8 +18,6 @@ using NuGetUpdater.Core.Utilities;
23
18
 
24
19
  namespace NuGetUpdater.Core;
25
20
 
26
- using EvaluationResult = (MSBuildHelper.EvaluationResultType ResultType, string EvaluatedValue, string? ErrorMessage);
27
-
28
21
  internal static partial class MSBuildHelper
29
22
  {
30
23
  public static string MSBuildPath { get; private set; } = string.Empty;
@@ -70,7 +63,7 @@ internal static partial class MSBuildHelper
70
63
  public static string[] GetTargetFrameworkMonikers(ImmutableArray<ProjectBuildFile> buildFiles)
71
64
  {
72
65
  HashSet<string> targetFrameworkValues = new(StringComparer.OrdinalIgnoreCase);
73
- Dictionary<string, string> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
66
+ Dictionary<string, Property> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
74
67
 
75
68
  foreach (var buildFile in buildFiles)
76
69
  {
@@ -81,6 +74,11 @@ internal static partial class MSBuildHelper
81
74
  if (property.Name.Equals("TargetFramework", StringComparison.OrdinalIgnoreCase) ||
82
75
  property.Name.Equals("TargetFrameworks", StringComparison.OrdinalIgnoreCase))
83
76
  {
77
+ if (buildFile.IsOutsideBasePath)
78
+ {
79
+ continue;
80
+ }
81
+
84
82
  foreach (var tfm in property.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
85
83
  {
86
84
  targetFrameworkValues.Add(tfm);
@@ -88,12 +86,17 @@ internal static partial class MSBuildHelper
88
86
  }
89
87
  else if (property.Name.Equals("TargetFrameworkVersion", StringComparison.OrdinalIgnoreCase))
90
88
  {
89
+ if (buildFile.IsOutsideBasePath)
90
+ {
91
+ continue;
92
+ }
93
+
91
94
  // For packages.config projects that use TargetFrameworkVersion, we need to convert it to TargetFramework
92
95
  targetFrameworkValues.Add($"net{property.Value.TrimStart('v').Replace(".", "")}");
93
96
  }
94
97
  else
95
98
  {
96
- propertyInfo[property.Name] = property.Value;
99
+ propertyInfo[property.Name] = new(property.Name, property.Value, buildFile.RelativePath);
97
100
  }
98
101
  }
99
102
  }
@@ -102,7 +105,7 @@ internal static partial class MSBuildHelper
102
105
 
103
106
  foreach (var targetFrameworkValue in targetFrameworkValues)
104
107
  {
105
- var (resultType, tfms, errorMessage) =
108
+ var (resultType, _, tfms, _, errorMessage) =
106
109
  GetEvaluatedValue(targetFrameworkValue, propertyInfo, propertiesToIgnore: ["TargetFramework", "TargetFrameworks"]);
107
110
  if (resultType != EvaluationResultType.Success)
108
111
  {
@@ -190,19 +193,65 @@ internal static partial class MSBuildHelper
190
193
  }
191
194
  }
192
195
 
196
+ public static IReadOnlyDictionary<string, Property> GetProperties(ImmutableArray<ProjectBuildFile> buildFiles)
197
+ {
198
+ Dictionary<string, Property> properties = new(StringComparer.OrdinalIgnoreCase);
199
+
200
+ foreach (var buildFile in buildFiles)
201
+ {
202
+ var projectRoot = CreateProjectRootElement(buildFile);
203
+
204
+ foreach (var property in projectRoot.Properties)
205
+ {
206
+ // Short of evaluating the entire project, there's no way to _really_ know what package version is
207
+ // going to be used, and even then we might not be able to update it. As a best guess, we'll simply
208
+ // skip any property that has a condition _or_ where the condition is checking for an empty string.
209
+ var hasEmptyCondition = string.IsNullOrEmpty(property.Condition);
210
+ var conditionIsCheckingForEmptyString = string.Equals(property.Condition, $"$({property.Name}) == ''", StringComparison.OrdinalIgnoreCase) ||
211
+ string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase);
212
+ if (hasEmptyCondition || conditionIsCheckingForEmptyString)
213
+ {
214
+ properties[property.Name] = new(property.Name, property.Value, buildFile.RelativePath);
215
+ }
216
+ }
217
+ }
218
+
219
+ return properties;
220
+ }
221
+
193
222
  public static IEnumerable<Dependency> GetTopLevelPackageDependencyInfos(ImmutableArray<ProjectBuildFile> buildFiles)
194
223
  {
195
- Dictionary<string, (string, bool)> packageInfo = new(StringComparer.OrdinalIgnoreCase);
224
+ Dictionary<string, (string, bool, DependencyType)> packageInfo = new(StringComparer.OrdinalIgnoreCase);
196
225
  Dictionary<string, string> packageVersionInfo = new(StringComparer.OrdinalIgnoreCase);
197
- Dictionary<string, string> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
226
+ Dictionary<string, Property> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
198
227
 
199
228
  foreach (var buildFile in buildFiles)
200
229
  {
201
230
  var projectRoot = CreateProjectRootElement(buildFile);
202
231
 
232
+ foreach (var property in projectRoot.Properties)
233
+ {
234
+ // Short of evaluating the entire project, there's no way to _really_ know what package version is
235
+ // going to be used, and even then we might not be able to update it. As a best guess, we'll simply
236
+ // skip any property that has a condition _or_ where the condition is checking for an empty string.
237
+ var hasEmptyCondition = string.IsNullOrEmpty(property.Condition);
238
+ var conditionIsCheckingForEmptyString = string.Equals(property.Condition, $"$({property.Name}) == ''", StringComparison.OrdinalIgnoreCase) ||
239
+ string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase);
240
+ if (hasEmptyCondition || conditionIsCheckingForEmptyString)
241
+ {
242
+ propertyInfo[property.Name] = new(property.Name, property.Value, buildFile.RelativePath);
243
+ }
244
+ }
245
+
246
+ if (buildFile.IsOutsideBasePath)
247
+ {
248
+ continue;
249
+ }
250
+
203
251
  foreach (var packageItem in projectRoot.Items
204
252
  .Where(i => (i.ItemType == "PackageReference" || i.ItemType == "GlobalPackageReference")))
205
253
  {
254
+ var dependencyType = packageItem.ItemType == "PackageReference" ? DependencyType.PackageReference : DependencyType.GlobalPackageReference;
206
255
  var versionSpecification = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value
207
256
  ?? packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase))?.Value
208
257
  ?? string.Empty;
@@ -219,12 +268,12 @@ internal static partial class MSBuildHelper
219
268
  var vSpec = string.IsNullOrEmpty(versionSpecification) || existingUpdate ? existingVersion : versionSpecification;
220
269
 
221
270
  var isUpdate = existingUpdate && string.IsNullOrEmpty(packageItem.Include);
222
- packageInfo[attributeValue] = (vSpec, isUpdate);
271
+ packageInfo[attributeValue] = (vSpec, isUpdate, dependencyType);
223
272
  }
224
273
  else
225
274
  {
226
275
  var isUpdate = !string.IsNullOrEmpty(packageItem.Update);
227
- packageInfo[attributeValue] = (versionSpecification, isUpdate);
276
+ packageInfo[attributeValue] = (versionSpecification, isUpdate, dependencyType);
228
277
  }
229
278
  }
230
279
  }
@@ -236,25 +285,11 @@ internal static partial class MSBuildHelper
236
285
  packageVersionInfo[packageItem.Include] = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value
237
286
  ?? string.Empty;
238
287
  }
239
-
240
- foreach (var property in projectRoot.Properties)
241
- {
242
- // Short of evaluating the entire project, there's no way to _really_ know what package version is
243
- // going to be used, and even then we might not be able to update it. As a best guess, we'll simply
244
- // skip any property that has a condition _or_ where the condition is checking for an empty string.
245
- var hasEmptyCondition = string.IsNullOrEmpty(property.Condition);
246
- var conditionIsCheckingForEmptyString = string.Equals(property.Condition, $"$({property.Name}) == ''", StringComparison.OrdinalIgnoreCase) ||
247
- string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase);
248
- if (hasEmptyCondition || conditionIsCheckingForEmptyString)
249
- {
250
- propertyInfo[property.Name] = property.Value;
251
- }
252
- }
253
288
  }
254
289
 
255
290
  foreach (var (name, info) in packageInfo)
256
291
  {
257
- var (version, isUpdate) = info;
292
+ var (version, isUpdate, dependencyType) = info;
258
293
  if (version.Length != 0 || !packageVersionInfo.TryGetValue(name, out var packageVersion))
259
294
  {
260
295
  packageVersion = version;
@@ -262,50 +297,51 @@ internal static partial class MSBuildHelper
262
297
 
263
298
  // Walk the property replacements until we don't find another one.
264
299
  var evaluationResult = GetEvaluatedValue(packageVersion, propertyInfo);
265
- if (evaluationResult.ResultType != EvaluationResultType.Success)
266
- {
267
- throw new InvalidDataException(evaluationResult.ErrorMessage);
268
- }
269
-
270
- packageVersion = evaluationResult.EvaluatedValue.TrimStart('[', '(').TrimEnd(']', ')');
300
+ packageVersion = evaluationResult.ResultType == EvaluationResultType.Success
301
+ ? evaluationResult.EvaluatedValue.TrimStart('[', '(').TrimEnd(']', ')')
302
+ : evaluationResult.EvaluatedValue;
271
303
 
272
304
  // We don't know the version for range requirements or wildcard
273
305
  // requirements, so return "" for these.
274
306
  yield return packageVersion.Contains(',') || packageVersion.Contains('*')
275
- ? new Dependency(name, string.Empty, DependencyType.Unknown, IsUpdate: isUpdate)
276
- : new Dependency(name, packageVersion, DependencyType.Unknown, IsUpdate: isUpdate);
307
+ ? new Dependency(name, string.Empty, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate)
308
+ : new Dependency(name, packageVersion, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate);
277
309
  }
278
310
  }
279
311
 
280
312
  /// <summary>
281
313
  /// Given an MSBuild string and a set of properties, returns our best guess at the final value MSBuild will evaluate to.
282
314
  /// </summary>
283
- public static EvaluationResult GetEvaluatedValue(string msbuildString, Dictionary<string, string> propertyInfo, params string[] propertiesToIgnore)
315
+ public static EvaluationResult GetEvaluatedValue(string msbuildString, IReadOnlyDictionary<string, Property> propertyInfo, params string[] propertiesToIgnore)
284
316
  {
285
317
  var ignoredProperties = new HashSet<string>(propertiesToIgnore, StringComparer.OrdinalIgnoreCase);
286
318
  var seenProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
287
319
 
320
+ string originalValue = msbuildString;
321
+ string? rootPropertyName = null;
288
322
  while (TryGetPropertyName(msbuildString, out var propertyName))
289
323
  {
324
+ rootPropertyName = propertyName;
325
+
290
326
  if (ignoredProperties.Contains(propertyName))
291
327
  {
292
- return (EvaluationResultType.PropertyIgnored, msbuildString, $"Property '{propertyName}' is ignored.");
328
+ return new(EvaluationResultType.PropertyIgnored, originalValue, msbuildString, rootPropertyName, $"Property '{propertyName}' is ignored.");
293
329
  }
294
330
 
295
331
  if (!seenProperties.Add(propertyName))
296
332
  {
297
- return (EvaluationResultType.CircularReference, msbuildString, $"Property '{propertyName}' has a circular reference.");
333
+ return new(EvaluationResultType.CircularReference, originalValue, msbuildString, rootPropertyName, $"Property '{propertyName}' has a circular reference.");
298
334
  }
299
335
 
300
- if (!propertyInfo.TryGetValue(propertyName, out var propertyValue))
336
+ if (!propertyInfo.TryGetValue(propertyName, out var property))
301
337
  {
302
- return (EvaluationResultType.PropertyNotFound, msbuildString, $"Property '{propertyName}' was not found.");
338
+ return new(EvaluationResultType.PropertyNotFound, originalValue, msbuildString, rootPropertyName, $"Property '{propertyName}' was not found.");
303
339
  }
304
340
 
305
- msbuildString = msbuildString.Replace($"$({propertyName})", propertyValue);
341
+ msbuildString = msbuildString.Replace($"$({propertyName})", property.Value);
306
342
  }
307
343
 
308
- return (EvaluationResultType.Success, msbuildString, null);
344
+ return new(EvaluationResultType.Success, originalValue, msbuildString, rootPropertyName, null);
309
345
  }
310
346
 
311
347
  public static bool TryGetPropertyName(string versionContent, [NotNullWhen(true)] out string? propertyName)
@@ -372,12 +408,13 @@ internal static partial class MSBuildHelper
372
408
  }
373
409
  }
374
410
 
375
- private static async Task<string> CreateTempProjectAsync(
411
+ internal static async Task<string> CreateTempProjectAsync(
376
412
  DirectoryInfo tempDir,
377
413
  string repoRoot,
378
414
  string projectPath,
379
415
  string targetFramework,
380
- IReadOnlyCollection<Dependency> packages)
416
+ IReadOnlyCollection<Dependency> packages,
417
+ bool usePackageDownload = false)
381
418
  {
382
419
  var projectDirectory = Path.GetDirectoryName(projectPath);
383
420
  projectDirectory ??= repoRoot;
@@ -409,9 +446,9 @@ internal static partial class MSBuildHelper
409
446
  Environment.NewLine,
410
447
  packages
411
448
  // empty `Version` attributes will cause the temporary project to not build
412
- .Where(p => !string.IsNullOrWhiteSpace(p.Version))
449
+ .Where(p => (p.EvaluationResult is null || p.EvaluationResult.ResultType == EvaluationResultType.Success) && !string.IsNullOrWhiteSpace(p.Version))
413
450
  // 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.
414
- .Select(static p => $"<PackageReference {(p.IsUpdate ? "Update" : "Include")}=\"{p.Name}\" Version=\"[{p.Version}]\" />"));
451
+ .Select(p => $"<{(usePackageDownload ? "PackageDownload" : "PackageReference")} {(p.IsUpdate ? "Update" : "Include")}=\"{p.Name}\" Version=\"[{p.Version}]\" />"));
415
452
 
416
453
  var projectContents = $"""
417
454
  <Project Sdk="Microsoft.NET.Sdk">
@@ -464,23 +501,34 @@ internal static partial class MSBuildHelper
464
501
  }
465
502
 
466
503
  internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(
467
- string repoRoot, string projectPath, string targetFramework, IReadOnlyCollection<Dependency> packages, Logger? logger = null)
504
+ string repoRoot,
505
+ string projectPath,
506
+ string targetFramework,
507
+ IReadOnlyCollection<Dependency> packages,
508
+ Logger? logger = null)
468
509
  {
469
510
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_");
470
511
  try
471
512
  {
513
+ var topLevelPackagesNames = packages.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
472
514
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
473
515
 
474
516
  var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\" /t:_ReportDependencies", workingDirectory: tempDirectory.FullName);
475
517
 
476
518
  if (exitCode == 0)
477
519
  {
520
+ ImmutableArray<string> tfms = [targetFramework];
478
521
  var lines = stdout.Split('\n').Select(line => line.Trim());
479
522
  var pattern = PackagePattern();
480
523
  var allDependencies = lines
481
524
  .Select(line => pattern.Match(line))
482
525
  .Where(match => match.Success)
483
- .Select(match => new Dependency(match.Groups["PackageName"].Value, match.Groups["PackageVersion"].Value, DependencyType.Unknown))
526
+ .Select(match =>
527
+ {
528
+ var packageName = match.Groups["PackageName"].Value;
529
+ var isTransitive = !topLevelPackagesNames.Contains(packageName);
530
+ return new Dependency(packageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, TargetFrameworks: tfms, IsTransitive: isTransitive);
531
+ })
484
532
  .ToArray();
485
533
 
486
534
  return allDependencies;
@@ -503,12 +551,25 @@ internal static partial class MSBuildHelper
503
551
  }
504
552
  }
505
553
 
506
- internal static string? GetGlobalJsonPath(string repoRootPath, string projectPath)
554
+ internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath)
507
555
  {
508
- return PathHelper.GetFileInDirectoryOrParent(Path.GetDirectoryName(projectPath)!, repoRootPath, "global.json");
556
+ globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json", caseSensitive: false);
557
+ return globalJsonPath is not null;
509
558
  }
510
559
 
511
- internal static async Task<ImmutableArray<ProjectBuildFile>> LoadBuildFiles(string repoRootPath, string projectPath)
560
+ internal static bool TryGetDotNetToolsJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? dotnetToolsJsonJsonPath)
561
+ {
562
+ dotnetToolsJsonJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./.config/dotnet-tools.json", caseSensitive: false);
563
+ return dotnetToolsJsonJsonPath is not null;
564
+ }
565
+
566
+ internal static bool TryGetDirectoryPackagesPropsPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? directoryPackagesPropsPath)
567
+ {
568
+ directoryPackagesPropsPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "./Directory.Packages.props", caseSensitive: false);
569
+ return directoryPackagesPropsPath is not null;
570
+ }
571
+
572
+ internal static async Task<ImmutableArray<ProjectBuildFile>> LoadBuildFilesAsync(string repoRootPath, string projectPath, bool includeSdkPropsAndTargets = false)
512
573
  {
513
574
  var buildFileList = new List<string>
514
575
  {
@@ -516,7 +577,7 @@ internal static partial class MSBuildHelper
516
577
  };
517
578
 
518
579
  // a global.json file might cause problems with the dotnet msbuild command; create a safe version temporarily
519
- var globalJsonPath = GetGlobalJsonPath(repoRootPath, projectPath);
580
+ TryGetGlobalJsonPath(repoRootPath, projectPath, out var globalJsonPath);
520
581
  var safeGlobalJsonName = $"{globalJsonPath}{Guid.NewGuid()}";
521
582
 
522
583
  try
@@ -566,12 +627,13 @@ internal static partial class MSBuildHelper
566
627
  }
567
628
 
568
629
  var repoRootPathPrefix = repoRootPath.NormalizePathToUnix() + "/";
569
- var buildFilesInRepo = buildFileList
570
- .Where(f => f.StartsWith(repoRootPathPrefix, StringComparison.OrdinalIgnoreCase))
630
+ var buildFiles = includeSdkPropsAndTargets
631
+ ? buildFileList.Distinct()
632
+ : buildFileList
633
+ .Where(f => f.StartsWith(repoRootPathPrefix, StringComparison.OrdinalIgnoreCase))
634
+ .Distinct();
635
+ var result = buildFiles
571
636
  .Where(File.Exists)
572
- .Distinct()
573
- .ToArray();
574
- var result = buildFilesInRepo
575
637
  .Select(path => ProjectBuildFile.Open(repoRootPath, path))
576
638
  .ToImmutableArray();
577
639
  return result;
@@ -579,12 +641,4 @@ internal static partial class MSBuildHelper
579
641
 
580
642
  [GeneratedRegex("^\\s*NuGetData::Package=(?<PackageName>[^,]+), Version=(?<PackageVersion>.+)$")]
581
643
  private static partial Regex PackagePattern();
582
-
583
- internal enum EvaluationResultType
584
- {
585
- Success,
586
- PropertyIgnored,
587
- CircularReference,
588
- PropertyNotFound,
589
- }
590
644
  }
@@ -1,4 +1,4 @@
1
- using System.IO;
1
+ using System.Diagnostics.CodeAnalysis;
2
2
 
3
3
  namespace NuGetUpdater.Core;
4
4
 
@@ -6,10 +6,33 @@ internal static class NuGetHelper
6
6
  {
7
7
  internal const string PackagesConfigFileName = "packages.config";
8
8
 
9
- public static bool HasPackagesConfigFile(string projectPath)
9
+ public static bool TryGetPackagesConfigFile(string projectPath, [NotNullWhen(returnValue: true)] out string? packagesConfigPath)
10
10
  {
11
11
  var projectDirectory = Path.GetDirectoryName(projectPath);
12
- var packagesConfigPath = PathHelper.JoinPath(projectDirectory, PackagesConfigFileName);
13
- return File.Exists(packagesConfigPath);
12
+
13
+ packagesConfigPath = PathHelper.JoinPath(projectDirectory, PackagesConfigFileName);
14
+ if (File.Exists(packagesConfigPath))
15
+ {
16
+ return true;
17
+ }
18
+
19
+ packagesConfigPath = null;
20
+ return false;
21
+ }
22
+
23
+ internal static async Task<bool> DownloadNuGetPackagesAsync(string repoRoot, string projectPath, IReadOnlyCollection<Dependency> packages, Logger logger)
24
+ {
25
+ var tempDirectory = Directory.CreateTempSubdirectory("msbuild_sdk_restore_");
26
+ try
27
+ {
28
+ var tempProjectPath = await MSBuildHelper.CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, "netstandard2.0", packages, usePackageDownload: true);
29
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\"");
30
+
31
+ return exitCode == 0;
32
+ }
33
+ finally
34
+ {
35
+ tempDirectory.Delete(recursive: true);
36
+ }
14
37
  }
15
38
  }
@@ -0,0 +1,117 @@
1
+ using System.Collections.Immutable;
2
+ using System.Text.Json;
3
+
4
+ using NuGetUpdater.Core.Discover;
5
+ using NuGetUpdater.Core.Test.Utilities;
6
+
7
+ using Xunit;
8
+
9
+ namespace NuGetUpdater.Core.Test.Discover;
10
+
11
+ using TestFile = (string Path, string Content);
12
+
13
+ public class DiscoveryWorkerTestBase
14
+ {
15
+ protected static async Task TestDiscoveryAsync(
16
+ string workspacePath,
17
+ TestFile[] files,
18
+ ExpectedWorkspaceDiscoveryResult expectedResult)
19
+ {
20
+ var actualResult = await RunDiscoveryAsync(files, async directoryPath =>
21
+ {
22
+ var worker = new DiscoveryWorker(new Logger(verbose: true));
23
+ await worker.RunAsync(directoryPath, workspacePath, DiscoveryWorker.DiscoveryResultFileName);
24
+ });
25
+
26
+ ValidateWorkspaceResult(expectedResult, actualResult);
27
+ }
28
+
29
+ protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult expectedResult, WorkspaceDiscoveryResult actualResult)
30
+ {
31
+ Assert.NotNull(actualResult);
32
+ Assert.Equal(expectedResult.FilePath, actualResult.FilePath);
33
+ ValidateDirectoryPackagesProps(expectedResult.DirectoryPackagesProps, actualResult.DirectoryPackagesProps);
34
+ ValidateResultWithDependencies(expectedResult.GlobalJson, actualResult.GlobalJson);
35
+ ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson);
36
+ ValidateProjectResults(expectedResult.Projects, actualResult.Projects);
37
+ Assert.Equal(expectedResult.ExpectedProjectCount ?? expectedResult.Projects.Length, actualResult.Projects.Length);
38
+
39
+ return;
40
+
41
+ void ValidateResultWithDependencies(ExpectedDependencyDiscoveryResult? expectedResult, IDiscoveryResultWithDependencies? actualResult)
42
+ {
43
+ if (expectedResult is null)
44
+ {
45
+ Assert.Null(actualResult);
46
+ return;
47
+ }
48
+ else
49
+ {
50
+ Assert.NotNull(actualResult);
51
+ }
52
+
53
+ Assert.Equal(expectedResult.FilePath, actualResult.FilePath);
54
+ ValidateDependencies(expectedResult.Dependencies, actualResult.Dependencies);
55
+ Assert.Equal(expectedResult.ExpectedDependencyCount ?? expectedResult.Dependencies.Length, actualResult.Dependencies.Length);
56
+ }
57
+
58
+ void ValidateProjectResults(ImmutableArray<ExpectedSdkProjectDiscoveryResult> expectedProjects, ImmutableArray<ProjectDiscoveryResult> actualProjects)
59
+ {
60
+ if (expectedProjects.IsDefaultOrEmpty)
61
+ {
62
+ return;
63
+ }
64
+
65
+ foreach (var expectedProject in expectedProjects)
66
+ {
67
+ var actualProject = actualProjects.Single(p => p.FilePath == expectedProject.FilePath);
68
+
69
+ Assert.Equal(expectedProject.FilePath, actualProject.FilePath);
70
+ AssertEx.Equal(expectedProject.Properties, actualProject.Properties);
71
+ AssertEx.Equal(expectedProject.TargetFrameworks, actualProject.TargetFrameworks);
72
+ AssertEx.Equal(expectedProject.ReferencedProjectPaths, actualProject.ReferencedProjectPaths);
73
+ ValidateDependencies(expectedProject.Dependencies, actualProject.Dependencies);
74
+ Assert.Equal(expectedProject.ExpectedDependencyCount ?? expectedProject.Dependencies.Length, actualProject.Dependencies.Length);
75
+ }
76
+ }
77
+
78
+ void ValidateDirectoryPackagesProps(ExpectedDirectoryPackagesPropsDiscovertyResult? expected, DirectoryPackagesPropsDiscoveryResult? actual)
79
+ {
80
+ ValidateResultWithDependencies(expected, actual);
81
+ Assert.Equal(expected?.IsTransitivePinningEnabled, actual?.IsTransitivePinningEnabled);
82
+ }
83
+
84
+ void ValidateDependencies(ImmutableArray<Dependency> expectedDependencies, ImmutableArray<Dependency> actualDependencies)
85
+ {
86
+ if (expectedDependencies.IsDefault)
87
+ {
88
+ return;
89
+ }
90
+
91
+ foreach (var expectedDependency in expectedDependencies)
92
+ {
93
+ var actualDependency = actualDependencies.Single(d => d.Name == expectedDependency.Name);
94
+ Assert.Equal(expectedDependency.Name, actualDependency.Name);
95
+ Assert.Equal(expectedDependency.Version, actualDependency.Version);
96
+ Assert.Equal(expectedDependency.Type, actualDependency.Type);
97
+ AssertEx.Equal(expectedDependency.TargetFrameworks, actualDependency.TargetFrameworks);
98
+ Assert.Equal(expectedDependency.IsDirect, actualDependency.IsDirect);
99
+ Assert.Equal(expectedDependency.IsTransitive, actualDependency.IsTransitive);
100
+ }
101
+ }
102
+ }
103
+
104
+ protected static async Task<WorkspaceDiscoveryResult> RunDiscoveryAsync(TestFile[] files, Func<string, Task> action)
105
+ {
106
+ // write initial files
107
+ using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files);
108
+
109
+ // run discovery
110
+ await action(temporaryDirectory.DirectoryPath);
111
+
112
+ // gather results
113
+ var resultPath = Path.Join(temporaryDirectory.DirectoryPath, DiscoveryWorker.DiscoveryResultFileName);
114
+ var resultJson = await File.ReadAllTextAsync(resultPath);
115
+ return JsonSerializer.Deserialize<WorkspaceDiscoveryResult>(resultJson, DiscoveryWorker.SerializerOptions)!;
116
+ }
117
+ }
@@ -0,0 +1,91 @@
1
+ using Xunit;
2
+
3
+ namespace NuGetUpdater.Core.Test.Discover;
4
+
5
+ public partial class DiscoveryWorkerTests
6
+ {
7
+ public class DotNetToolsJson : DiscoveryWorkerTestBase
8
+ {
9
+ [Fact]
10
+ public async Task DiscoversDependencies()
11
+ {
12
+ await TestDiscoveryAsync(
13
+ workspacePath: "",
14
+ files: [
15
+ (".config/dotnet-tools.json", """
16
+ {
17
+ "version": 1,
18
+ "isRoot": true,
19
+ "tools": {
20
+ "botsay": {
21
+ "version": "1.0.0",
22
+ "commands": [
23
+ "botsay"
24
+ ]
25
+ },
26
+ "dotnetsay": {
27
+ "version": "1.0.0",
28
+ "commands": [
29
+ "dotnetsay"
30
+ ]
31
+ }
32
+ }
33
+ }
34
+ """),
35
+ ],
36
+ expectedResult: new()
37
+ {
38
+ FilePath = "",
39
+ DotNetToolsJson = new()
40
+ {
41
+ FilePath = ".config/dotnet-tools.json",
42
+ Dependencies = [
43
+ new("botsay", "1.0.0", DependencyType.DotNetTool),
44
+ new("dotnetsay", "1.0.0", DependencyType.DotNetTool),
45
+ ]
46
+ },
47
+ ExpectedProjectCount = 0,
48
+ });
49
+ }
50
+
51
+ [Fact]
52
+ public async Task ReportsFailure()
53
+ {
54
+ await TestDiscoveryAsync(
55
+ workspacePath: "",
56
+ files: [
57
+ (".config/dotnet-tools.json", """
58
+ {
59
+ "version": 1,
60
+ "isRoot": true,
61
+ "tools": {
62
+ "botsay": {
63
+ "version": "1.0.0",
64
+ "commands": [
65
+ "botsay"
66
+ ],
67
+ },
68
+ "dotnetsay": {
69
+ "version": "1.0.0",
70
+ "commands": [
71
+ "dotnetsay"
72
+ ]
73
+ }
74
+ }
75
+ }
76
+ """),
77
+ ],
78
+ expectedResult: new()
79
+ {
80
+ FilePath = "",
81
+ DotNetToolsJson = new()
82
+ {
83
+ FilePath = ".config/dotnet-tools.json",
84
+ IsSuccess = false,
85
+ ExpectedDependencyCount = 0,
86
+ },
87
+ ExpectedProjectCount = 0,
88
+ });
89
+ }
90
+ }
91
+ }