dependabot-nuget 0.263.0 → 0.264.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs +37 -0
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +3 -3
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +1 -0
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs +169 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +79 -67
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs +0 -4
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +10 -11
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs +11 -0
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +441 -0
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs +177 -0
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs +47 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs +12 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs +36 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs +128 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs +105 -0
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs +17 -0
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs +11 -0
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs +36 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +179 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs +54 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +5 -2
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +2 -1
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +2 -2
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +0 -2
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +0 -3
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +0 -3
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +0 -5
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +0 -4
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +6 -2
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs +0 -4
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs +0 -2
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs +0 -2
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +0 -4
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs +0 -3
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +7 -8
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +0 -4
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +0 -3
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +0 -4
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs +90 -0
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +304 -0
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs +145 -0
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs +8 -0
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs +69 -0
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs +78 -0
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs +193 -0
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +1 -2
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs +2 -2
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs +2 -2
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +1 -1
  51. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs +1 -1
  52. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +102 -9
  53. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +4 -4
  54. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +2 -2
  55. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +8 -2
  56. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +2 -1
  57. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +8 -7
  58. data/lib/dependabot/nuget/analysis/analysis_json_reader.rb +63 -0
  59. data/lib/dependabot/nuget/analysis/dependency_analysis.rb +63 -0
  60. data/lib/dependabot/nuget/file_fetcher.rb +7 -6
  61. data/lib/dependabot/nuget/file_parser.rb +28 -21
  62. data/lib/dependabot/nuget/file_updater.rb +22 -25
  63. data/lib/dependabot/nuget/metadata_finder.rb +2 -160
  64. data/lib/dependabot/nuget/native_discovery/native_dependency_details.rb +102 -0
  65. data/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb +129 -0
  66. data/lib/dependabot/nuget/native_discovery/native_directory_packages_props_discovery.rb +44 -0
  67. data/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb +174 -0
  68. data/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb +63 -0
  69. data/lib/dependabot/nuget/native_discovery/native_project_discovery.rb +82 -0
  70. data/lib/dependabot/nuget/native_discovery/native_property_details.rb +43 -0
  71. data/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb +68 -0
  72. data/lib/dependabot/nuget/native_helpers.rb +59 -0
  73. data/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb +105 -0
  74. data/lib/dependabot/nuget/native_update_checker/native_update_checker.rb +200 -0
  75. data/lib/dependabot/nuget/nuget_config_credential_helpers.rb +3 -2
  76. data/lib/dependabot/nuget/update_checker.rb +47 -0
  77. metadata +39 -5
@@ -0,0 +1,441 @@
1
+ using System.Collections.Immutable;
2
+ using System.Text.Json;
3
+ using System.Text.Json.Serialization;
4
+
5
+ using NuGet.Configuration;
6
+ using NuGet.Frameworks;
7
+ using NuGet.Versioning;
8
+
9
+ using NuGetUpdater.Core.Discover;
10
+
11
+ namespace NuGetUpdater.Core.Analyze;
12
+
13
+ using MultiDependency = (string PropertyName, ImmutableArray<string> TargetFrameworks, ImmutableHashSet<string> DependencyNames);
14
+
15
+ public partial class AnalyzeWorker
16
+ {
17
+ public const string AnalysisDirectoryName = "./.dependabot/analysis";
18
+
19
+ private readonly Logger _logger;
20
+
21
+ internal static readonly JsonSerializerOptions SerializerOptions = new()
22
+ {
23
+ WriteIndented = true,
24
+ Converters = { new JsonStringEnumConverter(), new RequirementConverter() },
25
+ };
26
+
27
+ public AnalyzeWorker(Logger logger)
28
+ {
29
+ _logger = logger;
30
+ }
31
+
32
+ public async Task RunAsync(string repoRoot, string discoveryPath, string dependencyPath, string analysisDirectory)
33
+ {
34
+ var discovery = await DeserializeJsonFileAsync<WorkspaceDiscoveryResult>(discoveryPath, nameof(WorkspaceDiscoveryResult));
35
+ var dependencyInfo = await DeserializeJsonFileAsync<DependencyInfo>(dependencyPath, nameof(DependencyInfo));
36
+ var startingDirectory = PathHelper.JoinPath(repoRoot, discovery.Path);
37
+
38
+ _logger.Log($"Starting analysis of {dependencyInfo.Name}...");
39
+
40
+ // We need to find all projects which have the given dependency. Even in cases that they
41
+ // have it transitively may require that peer dependencies be updated in the project.
42
+ var projectsWithDependency = discovery.Projects
43
+ .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase)))
44
+ .ToImmutableArray();
45
+ var projectFrameworks = projectsWithDependency
46
+ .SelectMany(p => p.TargetFrameworks)
47
+ .Distinct()
48
+ .Select(NuGetFramework.Parse)
49
+ .ToImmutableArray();
50
+ var propertyBasedDependencies = discovery.Projects.SelectMany(p
51
+ => p.Dependencies.Where(d => !d.IsTransitive &&
52
+ d.EvaluationResult?.RootPropertyName is not null)
53
+ ).ToImmutableArray();
54
+
55
+ bool usesMultiDependencyProperty = false;
56
+ NuGetVersion? updatedVersion = null;
57
+ ImmutableArray<Dependency> updatedDependencies = [];
58
+
59
+ bool isUpdateNecessary = IsUpdateNecessary(dependencyInfo, projectsWithDependency);
60
+ if (isUpdateNecessary)
61
+ {
62
+ var nugetContext = new NuGetContext(startingDirectory);
63
+ if (!Directory.Exists(nugetContext.TempPackageDirectory))
64
+ {
65
+ Directory.CreateDirectory(nugetContext.TempPackageDirectory);
66
+ }
67
+
68
+ _logger.Log($" Determining multi-dependency property.");
69
+ var multiDependencies = DetermineMultiDependencyDetails(
70
+ discovery,
71
+ dependencyInfo.Name,
72
+ propertyBasedDependencies);
73
+
74
+ usesMultiDependencyProperty = multiDependencies.Any(md => md.DependencyNames.Count > 1);
75
+ var dependenciesToUpdate = usesMultiDependencyProperty
76
+ ? multiDependencies
77
+ .SelectMany(md => md.DependencyNames)
78
+ .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase)
79
+ : [dependencyInfo.Name];
80
+ var applicableTargetFrameworks = usesMultiDependencyProperty
81
+ ? multiDependencies
82
+ .SelectMany(md => md.TargetFrameworks)
83
+ .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase)
84
+ .Select(NuGetFramework.Parse)
85
+ .ToImmutableArray()
86
+ : projectFrameworks;
87
+
88
+ _logger.Log($" Finding updated version.");
89
+ updatedVersion = await FindUpdatedVersionAsync(
90
+ startingDirectory,
91
+ dependencyInfo,
92
+ dependenciesToUpdate,
93
+ applicableTargetFrameworks,
94
+ nugetContext,
95
+ _logger,
96
+ CancellationToken.None);
97
+
98
+ _logger.Log($" Finding updated peer dependencies.");
99
+ updatedDependencies = updatedVersion is not null
100
+ ? await FindUpdatedDependenciesAsync(
101
+ repoRoot,
102
+ discovery,
103
+ dependenciesToUpdate,
104
+ updatedVersion,
105
+ nugetContext,
106
+ _logger,
107
+ CancellationToken.None)
108
+ : [];
109
+
110
+ //TODO: At this point we should add the peer dependencies to a queue where
111
+ // we will analyze them one by one to see if they themselves are part of a
112
+ // multi-dependency property. Basically looping this if-body until we have
113
+ // emptied the queue and have a complete list of updated dependencies. We
114
+ // should track the dependenciesToUpdate as they have already been analyzed.
115
+ }
116
+
117
+ var result = new AnalysisResult
118
+ {
119
+ UpdatedVersion = updatedVersion?.ToNormalizedString() ?? dependencyInfo.Version,
120
+ CanUpdate = updatedVersion is not null,
121
+ VersionComesFromMultiDependencyProperty = usesMultiDependencyProperty,
122
+ UpdatedDependencies = updatedDependencies,
123
+ };
124
+
125
+ await WriteResultsAsync(analysisDirectory, dependencyInfo.Name, result, _logger);
126
+
127
+ _logger.Log($"Analysis complete.");
128
+ }
129
+
130
+ private static bool IsUpdateNecessary(DependencyInfo dependencyInfo, ImmutableArray<ProjectDiscoveryResult> projectsWithDependency)
131
+ {
132
+ if (projectsWithDependency.Length == 0)
133
+ {
134
+ return false;
135
+ }
136
+
137
+ // We will even attempt to update transitive dependencies if the dependency is vulnerable.
138
+ if (dependencyInfo.IsVulnerable)
139
+ {
140
+ return true;
141
+ }
142
+
143
+ // Since the dependency is not vulnerable, we only need to update if it is not transitive.
144
+ return projectsWithDependency.Any(p =>
145
+ p.Dependencies.Any(d =>
146
+ d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) &&
147
+ !d.IsTransitive));
148
+ }
149
+
150
+ internal static async Task<T> DeserializeJsonFileAsync<T>(string path, string fileType)
151
+ {
152
+ var json = File.Exists(path)
153
+ ? await File.ReadAllTextAsync(path)
154
+ : throw new FileNotFoundException($"{fileType} file not found.", path);
155
+
156
+ return JsonSerializer.Deserialize<T>(json, SerializerOptions)
157
+ ?? throw new InvalidOperationException($"{fileType} file is empty.");
158
+ }
159
+
160
+ internal static async Task<NuGetVersion?> FindUpdatedVersionAsync(
161
+ string startingDirectory,
162
+ DependencyInfo dependencyInfo,
163
+ ImmutableHashSet<string> packageIds,
164
+ ImmutableArray<NuGetFramework> projectFrameworks,
165
+ NuGetContext nugetContext,
166
+ Logger logger,
167
+ CancellationToken cancellationToken)
168
+ {
169
+ var versionResult = await VersionFinder.GetVersionsAsync(
170
+ dependencyInfo,
171
+ nugetContext,
172
+ logger,
173
+ cancellationToken);
174
+
175
+ return await FindUpdatedVersionAsync(
176
+ packageIds,
177
+ dependencyInfo.Version,
178
+ versionResult,
179
+ projectFrameworks,
180
+ findLowestVersion: dependencyInfo.IsVulnerable,
181
+ nugetContext,
182
+ logger,
183
+ cancellationToken);
184
+ }
185
+
186
+ internal static async Task<NuGetVersion?> FindUpdatedVersionAsync(
187
+ ImmutableHashSet<string> packageIds,
188
+ ImmutableArray<NuGetFramework> projectFrameworks,
189
+ NuGetVersion version,
190
+ bool findLowestVersion,
191
+ NuGetContext nugetContext,
192
+ Logger logger,
193
+ CancellationToken cancellationToken)
194
+ {
195
+ var versionResult = await VersionFinder.GetVersionsAsync(
196
+ packageIds.First(),
197
+ version,
198
+ nugetContext,
199
+ logger,
200
+ cancellationToken);
201
+
202
+ return await FindUpdatedVersionAsync(
203
+ packageIds,
204
+ version.ToNormalizedString(),
205
+ versionResult,
206
+ projectFrameworks,
207
+ findLowestVersion,
208
+ nugetContext,
209
+ logger,
210
+ cancellationToken);
211
+ }
212
+
213
+ internal static async Task<NuGetVersion?> FindUpdatedVersionAsync(
214
+ ImmutableHashSet<string> packageIds,
215
+ string versionString,
216
+ VersionResult versionResult,
217
+ ImmutableArray<NuGetFramework> projectFrameworks,
218
+ bool findLowestVersion,
219
+ NuGetContext nugetContext,
220
+ Logger logger,
221
+ CancellationToken cancellationToken)
222
+ {
223
+ var versions = versionResult.GetVersions();
224
+ var orderedVersions = findLowestVersion
225
+ ? versions.OrderBy(v => v) // If we are fixing a vulnerability, then we want the lowest version that is safe.
226
+ : versions.OrderByDescending(v => v); // If we are just updating versions, then we want the highest version possible.
227
+
228
+ return await FindFirstCompatibleVersion(
229
+ packageIds,
230
+ versionString,
231
+ versionResult,
232
+ orderedVersions,
233
+ projectFrameworks,
234
+ nugetContext,
235
+ logger,
236
+ cancellationToken);
237
+ }
238
+
239
+ internal static async Task<NuGetVersion?> FindFirstCompatibleVersion(
240
+ ImmutableHashSet<string> packageIds,
241
+ string versionString,
242
+ VersionResult versionResult,
243
+ IEnumerable<NuGetVersion> orderedVersions,
244
+ ImmutableArray<NuGetFramework> projectFrameworks,
245
+ NuGetContext nugetContext,
246
+ Logger logger,
247
+ CancellationToken cancellationToken)
248
+ {
249
+ if (NuGetVersion.TryParse(versionString, out var currentVersion))
250
+ {
251
+ var isCompatible = await AreAllPackagesCompatibleAsync(
252
+ packageIds,
253
+ currentVersion,
254
+ projectFrameworks,
255
+ nugetContext,
256
+ logger,
257
+ cancellationToken);
258
+
259
+ if (!isCompatible)
260
+ {
261
+ // If the current package is incompatible, then don't check for compatibility.
262
+ return orderedVersions.First();
263
+ }
264
+ }
265
+
266
+ foreach (var version in orderedVersions)
267
+ {
268
+ var existsForAll = await VersionFinder.DoVersionsExistAsync(packageIds, version, nugetContext, logger, cancellationToken);
269
+ if (!existsForAll)
270
+ {
271
+ continue;
272
+ }
273
+
274
+ var isCompatible = await AreAllPackagesCompatibleAsync(
275
+ packageIds,
276
+ version,
277
+ projectFrameworks,
278
+ nugetContext,
279
+ logger,
280
+ cancellationToken);
281
+
282
+ if (isCompatible)
283
+ {
284
+ return version;
285
+ }
286
+ }
287
+
288
+ // Could not find a compatible version
289
+ return null;
290
+ }
291
+
292
+ internal static async Task<bool> AreAllPackagesCompatibleAsync(
293
+ ImmutableHashSet<string> packageIds,
294
+ NuGetVersion currentVersion,
295
+ ImmutableArray<NuGetFramework> projectFrameworks,
296
+ NuGetContext nugetContext,
297
+ Logger logger,
298
+ CancellationToken cancellationToken)
299
+ {
300
+ foreach (var packageId in packageIds)
301
+ {
302
+ var isCompatible = await CompatibilityChecker.CheckAsync(
303
+ new(packageId, currentVersion),
304
+ projectFrameworks,
305
+ nugetContext,
306
+ logger,
307
+ cancellationToken);
308
+ if (!isCompatible)
309
+ {
310
+ return false;
311
+ }
312
+ }
313
+
314
+ return true;
315
+ }
316
+
317
+ internal static async Task<ImmutableDictionary<NuGetFramework, ImmutableArray<Dependency>>> GetDependenciesAsync(
318
+ string workspacePath,
319
+ string projectPath,
320
+ IEnumerable<NuGetFramework> frameworks,
321
+ Dependency package,
322
+ Logger logger)
323
+ {
324
+ var result = ImmutableDictionary.CreateBuilder<NuGetFramework, ImmutableArray<Dependency>>();
325
+ foreach (var framework in frameworks)
326
+ {
327
+ var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(
328
+ workspacePath,
329
+ projectPath,
330
+ framework.ToString(),
331
+ [package],
332
+ logger);
333
+ result.Add(framework, [.. dependencies]);
334
+ }
335
+ return result.ToImmutable();
336
+ }
337
+
338
+ internal static async Task<ImmutableArray<Dependency>> FindUpdatedDependenciesAsync(
339
+ string repoRoot,
340
+ WorkspaceDiscoveryResult discovery,
341
+ ImmutableHashSet<string> packageIds,
342
+ NuGetVersion updatedVersion,
343
+ NuGetContext nugetContext,
344
+ Logger logger,
345
+ CancellationToken cancellationToken)
346
+ {
347
+ // We need to find all projects which have the given dependency. Even in cases that they
348
+ // have it transitively may require that peer dependencies be updated in the project.
349
+ var projectsWithDependency = discovery.Projects
350
+ .Where(p => p.Dependencies.Any(d => packageIds.Contains(d.Name)))
351
+ .ToImmutableArray();
352
+ if (projectsWithDependency.Length == 0)
353
+ {
354
+ return [];
355
+ }
356
+
357
+ var projectFrameworks = projectsWithDependency
358
+ .SelectMany(p => p.TargetFrameworks)
359
+ .Distinct()
360
+ .Select(NuGetFramework.Parse)
361
+ .ToImmutableArray();
362
+
363
+ // When updating peer dependencies, we only need to consider top-level dependencies.
364
+ var projectDependencyNames = projectsWithDependency
365
+ .SelectMany(p => p.Dependencies)
366
+ .Where(d => !d.IsTransitive)
367
+ .Select(d => d.Name)
368
+ .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
369
+
370
+ // Determine updated peer dependencies
371
+ var workspacePath = PathHelper.JoinPath(repoRoot, discovery.Path);
372
+ // We need any project path so the dependency finder can locate the nuget.config
373
+ var projectPath = Path.Combine(workspacePath, projectsWithDependency.First().FilePath);
374
+
375
+ // Create distinct list of dependencies taking the highest version of each
376
+ var dependencyResult = await DependencyFinder.GetDependenciesAsync(
377
+ workspacePath,
378
+ projectPath,
379
+ projectFrameworks,
380
+ packageIds,
381
+ updatedVersion,
382
+ nugetContext,
383
+ logger,
384
+ cancellationToken);
385
+
386
+ // Filter dependencies by whether any project references them
387
+ var dependencies = dependencyResult.GetDependencies()
388
+ .Where(d => projectDependencyNames.Contains(d.Name))
389
+ .ToImmutableArray();
390
+
391
+ return dependencies;
392
+ }
393
+
394
+ internal static ImmutableArray<MultiDependency> DetermineMultiDependencyDetails(
395
+ WorkspaceDiscoveryResult discovery,
396
+ string packageId,
397
+ ImmutableArray<Dependency> propertyBasedDependencies)
398
+ {
399
+ var packageDeclarationsUsingProperty = discovery.Projects
400
+ .SelectMany(p =>
401
+ p.Dependencies.Where(d => !d.IsTransitive &&
402
+ d.Name.Equals(packageId, StringComparison.OrdinalIgnoreCase) &&
403
+ d.EvaluationResult?.RootPropertyName is not null)
404
+ ).ToImmutableArray();
405
+
406
+ return packageDeclarationsUsingProperty
407
+ .Select(d => d.EvaluationResult!.RootPropertyName!)
408
+ .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase)
409
+ .Select(property =>
410
+ {
411
+ // Find all dependencies that use the same property
412
+ var packages = propertyBasedDependencies
413
+ .Where(d => property.Equals(d.EvaluationResult?.RootPropertyName, StringComparison.OrdinalIgnoreCase));
414
+
415
+ // Combine all their target frameworks
416
+ var tfms = packages.SelectMany(d => d.TargetFrameworks ?? [])
417
+ .Distinct()
418
+ .ToImmutableArray();
419
+
420
+ var packageIds = packages.Select(d => d.Name)
421
+ .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
422
+
423
+ return (property, tfms, packageIds);
424
+ }).ToImmutableArray();
425
+ }
426
+
427
+ internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result, Logger logger)
428
+ {
429
+ if (!Directory.Exists(analysisDirectory))
430
+ {
431
+ Directory.CreateDirectory(analysisDirectory);
432
+ }
433
+
434
+ var resultPath = Path.Combine(analysisDirectory, $"{dependencyName}.json");
435
+
436
+ logger.Log($" Writing analysis result to [{resultPath}].");
437
+
438
+ var resultJson = JsonSerializer.Serialize(result, SerializerOptions);
439
+ await File.WriteAllTextAsync(path: resultPath, resultJson);
440
+ }
441
+ }
@@ -0,0 +1,177 @@
1
+ using System.Collections.Immutable;
2
+
3
+ using NuGet.Common;
4
+ using NuGet.Configuration;
5
+ using NuGet.Frameworks;
6
+ using NuGet.Packaging;
7
+ using NuGet.Packaging.Core;
8
+ using NuGet.Protocol;
9
+ using NuGet.Protocol.Core.Types;
10
+
11
+ using NuGetUpdater.Core.FrameworkChecker;
12
+
13
+ namespace NuGetUpdater.Core.Analyze;
14
+
15
+ using PackageInfo = (bool IsDevDependency, ImmutableArray<NuGetFramework> Frameworks);
16
+ using PackageReaders = (IAsyncPackageCoreReader CoreReader, IAsyncPackageContentReader ContentReader);
17
+
18
+ internal static class CompatibilityChecker
19
+ {
20
+ public static async Task<bool> CheckAsync(
21
+ PackageIdentity package,
22
+ ImmutableArray<NuGetFramework> projectFrameworks,
23
+ NuGetContext nugetContext,
24
+ Logger logger,
25
+ CancellationToken cancellationToken)
26
+ {
27
+ var (isDevDependency, packageFrameworks) = await GetPackageInfoAsync(
28
+ package,
29
+ nugetContext,
30
+ cancellationToken);
31
+
32
+ return PerformCheck(package, projectFrameworks, isDevDependency, packageFrameworks, logger);
33
+ }
34
+
35
+ internal static bool PerformCheck(
36
+ PackageIdentity package,
37
+ ImmutableArray<NuGetFramework> projectFrameworks,
38
+ bool isDevDependency,
39
+ ImmutableArray<NuGetFramework> packageFrameworks,
40
+ Logger logger)
41
+ {
42
+ // development dependencies are packages such as analyzers which need to be compatible with the compiler not the
43
+ // project itself, but some packages that report themselves as development dependencies still contain target
44
+ // framework dependencies and should be checked for compatibility through the regular means
45
+ if (isDevDependency && packageFrameworks.Length == 0)
46
+ {
47
+ return true;
48
+ }
49
+
50
+ if (packageFrameworks.Length == 0 || projectFrameworks.Length == 0)
51
+ {
52
+ return false;
53
+ }
54
+
55
+ var compatibilityService = new FrameworkCompatibilityService();
56
+ var compatibleFrameworks = compatibilityService.GetCompatibleFrameworks(packageFrameworks);
57
+
58
+ var incompatibleFrameworks = projectFrameworks.Where(f => !compatibleFrameworks.Contains(f)).ToArray();
59
+ if (incompatibleFrameworks.Length > 0)
60
+ {
61
+ logger.Log($"The package {package} is not compatible. Incompatible project frameworks: {string.Join(", ", incompatibleFrameworks.Select(f => f.GetShortFolderName()))}");
62
+ return false;
63
+ }
64
+
65
+ return true;
66
+ }
67
+
68
+ internal static async Task<PackageInfo> GetPackageInfoAsync(
69
+ PackageIdentity package,
70
+ NuGetContext nugetContext,
71
+ CancellationToken cancellationToken)
72
+ {
73
+ var tempPackagePath = GetTempPackagePath(package, nugetContext);
74
+ var readers = File.Exists(tempPackagePath)
75
+ ? ReadPackage(tempPackagePath)
76
+ : await DownloadPackageAsync(package, nugetContext, cancellationToken);
77
+
78
+ var nuspecStream = await readers.CoreReader.GetNuspecAsync(cancellationToken);
79
+ var reader = new NuspecReader(nuspecStream);
80
+
81
+ var isDevDependency = reader.GetDevelopmentDependency();
82
+
83
+ var tfms = reader.GetDependencyGroups()
84
+ .Select(d => d.TargetFramework)
85
+ .ToImmutableArray();
86
+ if (tfms.Length == 0)
87
+ {
88
+ // If the nuspec doesn't have any dependency groups,
89
+ // try to get the TargetFramework from files in the lib folder.
90
+ var libItems = (await readers.ContentReader.GetLibItemsAsync(cancellationToken)).ToList();
91
+ if (libItems.Count == 0)
92
+ {
93
+ // If there is no lib folder in this package, then assume it is a dev dependency.
94
+ isDevDependency = true;
95
+ }
96
+
97
+ tfms = libItems.Select(item => item.TargetFramework)
98
+ .Distinct()
99
+ .ToImmutableArray();
100
+ }
101
+
102
+ // The interfaces we given are not disposable but the underlying type can be.
103
+ // This will ensure we dispose of any resources that need to be cleaned up.
104
+ (readers.CoreReader as IDisposable)?.Dispose();
105
+ (readers.ContentReader as IDisposable)?.Dispose();
106
+
107
+ return (isDevDependency, tfms);
108
+ }
109
+
110
+ internal static PackageReaders ReadPackage(string tempPackagePath)
111
+ {
112
+ var stream = new FileStream(
113
+ tempPackagePath,
114
+ FileMode.Open,
115
+ FileAccess.Read,
116
+ FileShare.Read,
117
+ bufferSize: 4096);
118
+ PackageArchiveReader archiveReader = new(stream);
119
+ return (archiveReader, archiveReader);
120
+ }
121
+
122
+ internal static async Task<PackageReaders> DownloadPackageAsync(
123
+ PackageIdentity package,
124
+ NuGetContext context,
125
+ CancellationToken cancellationToken)
126
+ {
127
+ var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(context.Settings);
128
+ var packageSources = sourceMapping.GetConfiguredPackageSources(package.Id).ToHashSet();
129
+ var sources = packageSources.Count == 0
130
+ ? context.PackageSources
131
+ : context.PackageSources
132
+ .Where(p => packageSources.Contains(p.Name))
133
+ .ToImmutableArray();
134
+
135
+ foreach (var source in sources)
136
+ {
137
+ var sourceRepository = Repository.Factory.GetCoreV3(source);
138
+ var feed = await sourceRepository.GetResourceAsync<FindPackageByIdResource>();
139
+ if (feed is null)
140
+ {
141
+ throw new NotSupportedException($"Failed to get FindPackageByIdResource for {source.SourceUri}");
142
+ }
143
+
144
+ var exists = await feed.DoesPackageExistAsync(
145
+ package.Id,
146
+ package.Version,
147
+ context.SourceCacheContext,
148
+ NullLogger.Instance,
149
+ cancellationToken);
150
+
151
+ if (!exists)
152
+ {
153
+ continue;
154
+ }
155
+
156
+ var downloader = await feed.GetPackageDownloaderAsync(
157
+ package,
158
+ context.SourceCacheContext,
159
+ context.Logger,
160
+ cancellationToken);
161
+
162
+ var tempPackagePath = GetTempPackagePath(package, context);
163
+ var isDownloaded = await downloader.CopyNupkgFileToAsync(tempPackagePath, cancellationToken);
164
+ if (!isDownloaded)
165
+ {
166
+ throw new Exception($"Failed to download package [{package.Id}/{package.Version}] from [${source.SourceUri}]");
167
+ }
168
+
169
+ return (downloader.CoreReader, downloader.ContentReader);
170
+ }
171
+
172
+ throw new Exception($"Package [{package.Id}/{package.Version}] does not exist in any of the configured sources.");
173
+ }
174
+
175
+ internal static string GetTempPackagePath(PackageIdentity package, NuGetContext context)
176
+ => Path.Combine(context.TempPackageDirectory, package.Id + "." + package.Version + ".nupkg");
177
+ }
@@ -0,0 +1,47 @@
1
+ using System.Collections.Immutable;
2
+
3
+ using NuGet.Frameworks;
4
+ using NuGet.Versioning;
5
+
6
+ namespace NuGetUpdater.Core.Analyze;
7
+
8
+ internal static class DependencyFinder
9
+ {
10
+ public static async Task<ImmutableDictionary<NuGetFramework, ImmutableArray<Dependency>>> GetDependenciesAsync(
11
+ string workspacePath,
12
+ string projectPath,
13
+ IEnumerable<NuGetFramework> frameworks,
14
+ ImmutableHashSet<string> packageIds,
15
+ NuGetVersion version,
16
+ NuGetContext nugetContext,
17
+ Logger logger,
18
+ CancellationToken cancellationToken)
19
+ {
20
+ var versionString = version.ToNormalizedString();
21
+ var packages = packageIds
22
+ .Select(id => new Dependency(id, versionString, DependencyType.Unknown))
23
+ .ToImmutableArray();
24
+
25
+ var result = ImmutableDictionary.CreateBuilder<NuGetFramework, ImmutableArray<Dependency>>();
26
+ foreach (var framework in frameworks)
27
+ {
28
+ var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(
29
+ workspacePath,
30
+ projectPath,
31
+ framework.ToString(),
32
+ packages,
33
+ logger);
34
+ var updatedDependencies = new List<Dependency>();
35
+ foreach (var dependency in dependencies)
36
+ {
37
+ var infoUrl = await nugetContext.GetPackageInfoUrlAsync(dependency.Name, dependency.Version!, cancellationToken);
38
+ var updatedDependency = dependency with { IsTransitive = false, InfoUrl = infoUrl };
39
+ updatedDependencies.Add(updatedDependency);
40
+ }
41
+
42
+ result.Add(framework, updatedDependencies.ToImmutableArray());
43
+ }
44
+
45
+ return result.ToImmutable();
46
+ }
47
+ }
@@ -0,0 +1,12 @@
1
+ using System.Collections.Immutable;
2
+
3
+ namespace NuGetUpdater.Core.Analyze;
4
+
5
+ public sealed record DependencyInfo
6
+ {
7
+ public required string Name { get; init; }
8
+ public required string Version { get; init; }
9
+ public required bool IsVulnerable { get; init; }
10
+ public ImmutableArray<Requirement> IgnoredVersions { get; init; }
11
+ public ImmutableArray<SecurityVulnerability> Vulnerabilities { get; init; }
12
+ }