dependabot-nuget 0.302.0 → 0.303.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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/Directory.Packages.props +5 -5
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyDiscoveryTargetingPacks.props +10 -0
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +96 -97
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +2 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +3 -0
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +1 -0
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +8 -0
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +7 -4
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/LockFileUpdater.cs +2 -0
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +257 -37
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +12 -3
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationBase.cs +209 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationResult.cs +3 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +79 -24
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +11 -11
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +19 -5
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +24 -6
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackageReferenceUpdaterTests.cs +177 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateOperationBaseTests.cs +130 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +5 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +71 -5
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackageReference.cs +87 -3
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +23 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +145 -147
  26. metadata +11 -7
@@ -0,0 +1,209 @@
1
+ using System.Collections.Immutable;
2
+ using System.Diagnostics.CodeAnalysis;
3
+ using System.Text.Json.Serialization;
4
+
5
+ using NuGet.Versioning;
6
+
7
+ using NuGetUpdater.Core.Utilities;
8
+
9
+
10
+ namespace NuGetUpdater.Core.Updater;
11
+
12
+ [JsonDerivedType(typeof(DirectUpdate))]
13
+ [JsonDerivedType(typeof(PinnedUpdate))]
14
+ [JsonDerivedType(typeof(ParentUpdate))]
15
+ public abstract record UpdateOperationBase
16
+ {
17
+ public abstract string Type { get; }
18
+ public required string DependencyName { get; init; }
19
+ public required NuGetVersion NewVersion { get; init; }
20
+ public required ImmutableArray<string> UpdatedFiles { get; init; }
21
+
22
+ public abstract string GetReport();
23
+
24
+ internal static string GenerateUpdateOperationReport(IEnumerable<UpdateOperationBase> updateOperations)
25
+ {
26
+ var updateMessages = updateOperations.Select(u => u.GetReport()).ToImmutableArray();
27
+ if (updateMessages.Length == 0)
28
+ {
29
+ return string.Empty;
30
+ }
31
+
32
+ var separator = "\n ";
33
+ var report = $"Performed the following updates:{separator}{string.Join(separator, updateMessages.Select(m => $"- {m}"))}";
34
+ return report;
35
+ }
36
+
37
+ internal static ImmutableArray<UpdateOperationBase> NormalizeUpdateOperationCollection(string repoRootPath, IEnumerable<UpdateOperationBase> updateOperations)
38
+ {
39
+ var groupedByKindWithCombinedFiles = updateOperations
40
+ .GroupBy(u => (u.GetType(), u.DependencyName, u.NewVersion))
41
+ .Select(g =>
42
+ {
43
+ if (g.Key.Item1 == typeof(DirectUpdate))
44
+ {
45
+ return new DirectUpdate()
46
+ {
47
+ DependencyName = g.Key.DependencyName,
48
+ NewVersion = g.Key.NewVersion,
49
+ UpdatedFiles = [.. g.SelectMany(u => u.UpdatedFiles)],
50
+ } as UpdateOperationBase;
51
+ }
52
+ else if (g.Key.Item1 == typeof(PinnedUpdate))
53
+ {
54
+ return new PinnedUpdate()
55
+ {
56
+ DependencyName = g.Key.DependencyName,
57
+ NewVersion = g.Key.NewVersion,
58
+ UpdatedFiles = [.. g.SelectMany(u => u.UpdatedFiles)],
59
+ };
60
+ }
61
+ else if (g.Key.Item1 == typeof(ParentUpdate))
62
+ {
63
+ var parentUpdate = (ParentUpdate)g.First();
64
+ return new ParentUpdate()
65
+ {
66
+ DependencyName = g.Key.DependencyName,
67
+ NewVersion = g.Key.NewVersion,
68
+ UpdatedFiles = [.. g.SelectMany(u => u.UpdatedFiles)],
69
+ ParentDependencyName = parentUpdate.ParentDependencyName,
70
+ ParentNewVersion = parentUpdate.ParentNewVersion,
71
+ };
72
+ }
73
+ else
74
+ {
75
+ throw new NotImplementedException(g.Key.Item1.FullName);
76
+ }
77
+ })
78
+ .ToImmutableArray();
79
+ var withNormalizedAndDistinctPaths = groupedByKindWithCombinedFiles
80
+ .Select(u => u with { UpdatedFiles = [.. u.UpdatedFiles.Select(f => Path.GetRelativePath(repoRootPath, f).FullyNormalizedRootedPath()).Distinct(PathComparer.Instance).OrderBy(f => f, StringComparer.Ordinal)] })
81
+ .ToImmutableArray();
82
+ var uniqueUpdateOperations = withNormalizedAndDistinctPaths.Distinct(UpdateOperationBaseComparer.Instance).ToImmutableArray();
83
+ var ordered = uniqueUpdateOperations
84
+ .OrderBy(u => u.GetType().Name)
85
+ .ThenBy(u => u.DependencyName)
86
+ .ThenBy(u => u.NewVersion)
87
+ .ThenBy(u => u.UpdatedFiles.Length)
88
+ .ThenBy(u => string.Join(",", u.UpdatedFiles))
89
+ .ThenBy(u => u is ParentUpdate parentUpdate ? parentUpdate.ParentDependencyName : string.Empty)
90
+ .ThenBy(u => u is ParentUpdate parentUpdate ? parentUpdate.ParentNewVersion : u.NewVersion)
91
+ .ToImmutableArray();
92
+ return ordered;
93
+ }
94
+
95
+ public override int GetHashCode()
96
+ {
97
+ var hash = new HashCode();
98
+ hash.Add(DependencyName);
99
+ hash.Add(NewVersion);
100
+ hash.Add(UpdatedFiles.Length);
101
+ for (int i = 0; i < UpdatedFiles.Length; i++)
102
+ {
103
+ hash.Add(UpdatedFiles[i]);
104
+ }
105
+
106
+ return hash.ToHashCode();
107
+ }
108
+
109
+ protected string GetString() => $"{GetType().Name} {{ {nameof(DependencyName)} = {DependencyName}, {nameof(NewVersion)} = {NewVersion}, {nameof(UpdatedFiles)} = {string.Join(",", UpdatedFiles)} }}";
110
+ }
111
+
112
+ public record DirectUpdate : UpdateOperationBase
113
+ {
114
+ public override string Type => nameof(DirectUpdate);
115
+ public override string GetReport() => $"Updated {DependencyName} to {NewVersion} in {string.Join("", UpdatedFiles)}";
116
+ public sealed override string ToString() => GetString();
117
+ }
118
+
119
+ public record PinnedUpdate : UpdateOperationBase
120
+ {
121
+ public override string Type => nameof(PinnedUpdate);
122
+ public override string GetReport() => $"Pinned {DependencyName} at {NewVersion} in {string.Join("", UpdatedFiles)}";
123
+ public sealed override string ToString() => GetString();
124
+ }
125
+
126
+ public record ParentUpdate : UpdateOperationBase, IEquatable<UpdateOperationBase>
127
+ {
128
+ public override string Type => nameof(ParentUpdate);
129
+ public required string ParentDependencyName { get; init; }
130
+ public required NuGetVersion ParentNewVersion { get; init; }
131
+
132
+ public override string GetReport() => $"Updated {DependencyName} to {NewVersion} indirectly via {ParentDependencyName}/{ParentNewVersion} in {string.Join("", UpdatedFiles)}";
133
+
134
+ bool IEquatable<UpdateOperationBase>.Equals(UpdateOperationBase? other)
135
+ {
136
+ if (!base.Equals(other))
137
+ {
138
+ return false;
139
+ }
140
+
141
+ if (other is not ParentUpdate otherParentUpdate)
142
+ {
143
+ return false;
144
+ }
145
+
146
+ return ParentDependencyName == otherParentUpdate.ParentDependencyName
147
+ && ParentNewVersion == otherParentUpdate.ParentNewVersion;
148
+ }
149
+
150
+ public override int GetHashCode()
151
+ {
152
+ var hash = new HashCode();
153
+ hash.Add(base.GetHashCode());
154
+ hash.Add(ParentDependencyName);
155
+ hash.Add(ParentNewVersion);
156
+ return hash.ToHashCode();
157
+ }
158
+
159
+ public sealed override string ToString() => $"{GetType().Name} {{ {nameof(DependencyName)} = {DependencyName}, {nameof(NewVersion)} = {NewVersion}, {nameof(ParentDependencyName)} = {ParentDependencyName}, {nameof(ParentNewVersion)} = {ParentNewVersion}, {nameof(UpdatedFiles)} = {string.Join(",", UpdatedFiles)} }}";
160
+ }
161
+
162
+ public class UpdateOperationBaseComparer : IEqualityComparer<UpdateOperationBase>
163
+ {
164
+ public static UpdateOperationBaseComparer Instance = new();
165
+
166
+ public bool Equals(UpdateOperationBase? x, UpdateOperationBase? y)
167
+ {
168
+ if (x is null && y is null)
169
+ {
170
+ return true;
171
+ }
172
+
173
+ if (x is null || y is null)
174
+ {
175
+ return false;
176
+ }
177
+
178
+ if (ReferenceEquals(x, y))
179
+ {
180
+ return true;
181
+ }
182
+
183
+ if (x.GetType() != y.GetType())
184
+ {
185
+ return false;
186
+ }
187
+
188
+ if (x.DependencyName != y.DependencyName ||
189
+ x.NewVersion != y.NewVersion ||
190
+ !x.UpdatedFiles.SequenceEqual(y.UpdatedFiles))
191
+ {
192
+ return false;
193
+ }
194
+
195
+ if (x is ParentUpdate px && y is ParentUpdate py)
196
+ {
197
+ // the `.GetType()` check above ensures this is safe
198
+ if (px.ParentDependencyName != py.ParentDependencyName ||
199
+ px.ParentNewVersion != py.ParentNewVersion)
200
+ {
201
+ return false;
202
+ }
203
+ }
204
+
205
+ return true;
206
+ }
207
+
208
+ public int GetHashCode([DisallowNull] UpdateOperationBase obj) => obj.GetHashCode();
209
+ }
@@ -1,5 +1,8 @@
1
+ using System.Collections.Immutable;
2
+
1
3
  namespace NuGetUpdater.Core.Updater;
2
4
 
3
5
  public record UpdateOperationResult : NativeResult
4
6
  {
7
+ public required ImmutableArray<UpdateOperationBase> UpdateOperations { get; init; }
5
8
  }
@@ -1,3 +1,4 @@
1
+ using System.Collections.Immutable;
1
2
  using System.Net;
2
3
  using System.Text.Json;
3
4
  using System.Text.Json.Serialization;
@@ -19,7 +20,7 @@ public class UpdaterWorker : IUpdaterWorker
19
20
  internal static readonly JsonSerializerOptions SerializerOptions = new()
20
21
  {
21
22
  WriteIndented = true,
22
- Converters = { new JsonStringEnumConverter() },
23
+ Converters = { new JsonStringEnumConverter(), new VersionConverter() },
23
24
  };
24
25
 
25
26
  public UpdaterWorker(string jobId, ExperimentsManager experimentsManager, ILogger logger)
@@ -41,10 +42,10 @@ public class UpdaterWorker : IUpdaterWorker
41
42
  // this is a convenient method for tests
42
43
  internal async Task<UpdateOperationResult> RunWithErrorHandlingAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive)
43
44
  {
44
- UpdateOperationResult result = new(); // assumed to be ok until proven otherwise
45
45
  try
46
46
  {
47
- result = await RunAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
47
+ var result = await RunAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
48
+ return result;
48
49
  }
49
50
  catch (Exception ex)
50
51
  {
@@ -52,13 +53,15 @@ public class UpdaterWorker : IUpdaterWorker
52
53
  {
53
54
  workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath));
54
55
  }
55
- result = new()
56
+
57
+ var error = JobErrorBase.ErrorFromException(ex, _jobId, workspacePath);
58
+ var result = new UpdateOperationResult()
56
59
  {
57
- Error = JobErrorBase.ErrorFromException(ex, _jobId, workspacePath),
60
+ UpdateOperations = [],
61
+ Error = error,
58
62
  };
63
+ return result;
59
64
  }
60
-
61
- return result;
62
65
  }
63
66
 
64
67
  public async Task<UpdateOperationResult> RunAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive)
@@ -76,40 +79,60 @@ public class UpdaterWorker : IUpdaterWorker
76
79
  await GlobalJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
77
80
  }
78
81
 
82
+ UpdateOperationResult result;
79
83
  var extension = Path.GetExtension(workspacePath).ToLowerInvariant();
80
84
  switch (extension)
81
85
  {
82
86
  case ".sln":
83
- await RunForSolutionAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
87
+ result = await RunForSolutionAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
84
88
  break;
85
89
  case ".proj":
86
- await RunForProjFileAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
90
+ result = await RunForProjFileAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
87
91
  break;
88
92
  case ".csproj":
89
93
  case ".fsproj":
90
94
  case ".vbproj":
91
- await RunForProjectAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
95
+ result = await RunForProjectAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
92
96
  break;
93
97
  default:
94
98
  _logger.Info($"File extension [{extension}] is not supported.");
99
+ result = new UpdateOperationResult()
100
+ {
101
+ UpdateOperations = [],
102
+ };
95
103
  break;
96
104
  }
97
105
 
106
+ result = result with { UpdateOperations = UpdateOperationBase.NormalizeUpdateOperationCollection(repoRootPath, result.UpdateOperations) };
107
+
108
+ if (!_experimentsManager.NativeUpdater)
109
+ {
110
+ // native updater reports the changes elsewhere
111
+ var updateReport = UpdateOperationBase.GenerateUpdateOperationReport(result.UpdateOperations);
112
+ _logger.Info(updateReport);
113
+ }
114
+
98
115
  _logger.Info("Update complete.");
99
116
 
100
117
  _processedProjectPaths.Clear();
101
- return new UpdateOperationResult();
118
+ return result;
119
+ }
120
+
121
+ internal static string Serialize(UpdateOperationResult result)
122
+ {
123
+ var resultJson = JsonSerializer.Serialize(result, SerializerOptions);
124
+ return resultJson;
102
125
  }
103
126
 
104
127
  internal static async Task WriteResultFile(UpdateOperationResult result, string resultOutputPath, ILogger logger)
105
128
  {
106
129
  logger.Info($" Writing update result to [{resultOutputPath}].");
107
130
 
108
- var resultJson = JsonSerializer.Serialize(result, SerializerOptions);
131
+ var resultJson = Serialize(result);
109
132
  await File.WriteAllTextAsync(resultOutputPath, resultJson);
110
133
  }
111
134
 
112
- private async Task RunForSolutionAsync(
135
+ private async Task<UpdateOperationResult> RunForSolutionAsync(
113
136
  string repoRootPath,
114
137
  string solutionPath,
115
138
  string dependencyName,
@@ -118,14 +141,21 @@ public class UpdaterWorker : IUpdaterWorker
118
141
  bool isTransitive)
119
142
  {
120
143
  _logger.Info($"Running for solution [{Path.GetRelativePath(repoRootPath, solutionPath)}]");
144
+ var updateOperations = new List<UpdateOperationBase>();
121
145
  var projectPaths = MSBuildHelper.GetProjectPathsFromSolution(solutionPath);
122
146
  foreach (var projectPath in projectPaths)
123
147
  {
124
- await RunForProjectAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
148
+ var projectResult = await RunForProjectAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
149
+ updateOperations.AddRange(projectResult.UpdateOperations);
125
150
  }
151
+
152
+ return new UpdateOperationResult()
153
+ {
154
+ UpdateOperations = updateOperations.ToImmutableArray(),
155
+ };
126
156
  }
127
157
 
128
- private async Task RunForProjFileAsync(
158
+ private async Task<UpdateOperationResult> RunForProjFileAsync(
129
159
  string repoRootPath,
130
160
  string projFilePath,
131
161
  string dependencyName,
@@ -137,21 +167,31 @@ public class UpdaterWorker : IUpdaterWorker
137
167
  if (!File.Exists(projFilePath))
138
168
  {
139
169
  _logger.Info($"File [{projFilePath}] does not exist.");
140
- return;
170
+ return new UpdateOperationResult()
171
+ {
172
+ UpdateOperations = [],
173
+ };
141
174
  }
142
175
 
176
+ var updateOperations = new List<UpdateOperationBase>();
143
177
  var projectFilePaths = MSBuildHelper.GetProjectPathsFromProject(projFilePath);
144
178
  foreach (var projectFullPath in projectFilePaths)
145
179
  {
146
180
  // If there is some MSBuild logic that needs to run to fully resolve the path skip the project
147
181
  if (File.Exists(projectFullPath))
148
182
  {
149
- await RunForProjectAsync(repoRootPath, projectFullPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
183
+ var projectResult = await RunForProjectAsync(repoRootPath, projectFullPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
184
+ updateOperations.AddRange(projectResult.UpdateOperations);
150
185
  }
151
186
  }
187
+
188
+ return new UpdateOperationResult()
189
+ {
190
+ UpdateOperations = updateOperations.ToImmutableArray(),
191
+ };
152
192
  }
153
193
 
154
- private async Task RunForProjectAsync(
194
+ private async Task<UpdateOperationResult> RunForProjectAsync(
155
195
  string repoRootPath,
156
196
  string projectPath,
157
197
  string dependencyName,
@@ -163,21 +203,31 @@ public class UpdaterWorker : IUpdaterWorker
163
203
  if (!File.Exists(projectPath))
164
204
  {
165
205
  _logger.Info($"File [{projectPath}] does not exist.");
166
- return;
206
+ return new UpdateOperationResult()
207
+ {
208
+ UpdateOperations = [],
209
+ };
167
210
  }
168
211
 
212
+ var updateOperations = new List<UpdateOperationBase>();
169
213
  var projectFilePaths = MSBuildHelper.GetProjectPathsFromProject(projectPath);
170
214
  foreach (var projectFullPath in projectFilePaths.Concat([projectPath]))
171
215
  {
172
216
  // If there is some MSBuild logic that needs to run to fully resolve the path skip the project
173
217
  if (File.Exists(projectFullPath))
174
218
  {
175
- await RunUpdaterAsync(repoRootPath, projectFullPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
219
+ var performedOperations = await RunUpdaterAsync(repoRootPath, projectFullPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
220
+ updateOperations.AddRange(performedOperations);
176
221
  }
177
222
  }
223
+
224
+ return new UpdateOperationResult()
225
+ {
226
+ UpdateOperations = updateOperations.ToImmutableArray(),
227
+ };
178
228
  }
179
229
 
180
- private async Task RunUpdaterAsync(
230
+ private async Task<IEnumerable<UpdateOperationBase>> RunUpdaterAsync(
181
231
  string repoRootPath,
182
232
  string projectPath,
183
233
  string dependencyName,
@@ -187,22 +237,25 @@ public class UpdaterWorker : IUpdaterWorker
187
237
  {
188
238
  if (_processedProjectPaths.Contains(projectPath))
189
239
  {
190
- return;
240
+ return [];
191
241
  }
192
242
 
193
243
  _processedProjectPaths.Add(projectPath);
194
244
 
195
245
  _logger.Info($"Updating project [{projectPath}]");
196
246
 
247
+ var updateOperations = new List<UpdateOperationBase>();
197
248
  var additionalFiles = ProjectHelper.GetAllAdditionalFilesFromProject(projectPath, ProjectHelper.PathFormat.Full);
198
249
  var packagesConfigFullPath = additionalFiles.Where(p => Path.GetFileName(p).Equals(ProjectHelper.PackagesConfigFileName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
199
250
  if (packagesConfigFullPath is not null)
200
251
  {
201
- await PackagesConfigUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, packagesConfigFullPath, _logger);
252
+ var packagesConfigOperations = await PackagesConfigUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, packagesConfigFullPath, _logger);
253
+ updateOperations.AddRange(packagesConfigOperations);
202
254
  }
203
255
 
204
256
  // Some repos use a mix of packages.config and PackageReference
205
- await PackageReferenceUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, _experimentsManager, _logger);
257
+ var packageReferenceOperations = await PackageReferenceUpdater.UpdateDependencyAsync(repoRootPath, projectPath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive, _experimentsManager, _logger);
258
+ updateOperations.AddRange(packageReferenceOperations);
206
259
 
207
260
  // Update lock file if exists
208
261
  var packagesLockFullPath = additionalFiles.Where(p => Path.GetFileName(p).Equals(ProjectHelper.PackagesLockJsonFileName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
@@ -210,5 +263,7 @@ public class UpdaterWorker : IUpdaterWorker
210
263
  {
211
264
  await LockFileUpdater.UpdateLockFileAsync(repoRootPath, projectPath, _experimentsManager, _logger);
212
265
  }
266
+
267
+ return updateOperations;
213
268
  }
214
269
  }
@@ -341,7 +341,7 @@ internal static partial class MSBuildHelper
341
341
  return false;
342
342
  }
343
343
 
344
- internal static async Task<bool> DependenciesAreCoherentAsync(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, ExperimentsManager experimentsManager, ILogger logger)
344
+ internal static async Task<bool> DependenciesAreCoherentAsync(string repoRoot, string projectPath, string targetFramework, ImmutableArray<Dependency> packages, ExperimentsManager experimentsManager, ILogger logger)
345
345
  {
346
346
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
347
347
  try
@@ -359,7 +359,7 @@ internal static partial class MSBuildHelper
359
359
  }
360
360
  }
361
361
 
362
- internal static async Task<Dependency[]?> ResolveDependencyConflicts(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Dependency[] update, ExperimentsManager experimentsManager, ILogger logger)
362
+ internal static async Task<ImmutableArray<Dependency>?> ResolveDependencyConflicts(string repoRoot, string projectPath, string targetFramework, ImmutableArray<Dependency> packages, ImmutableArray<Dependency> update, ExperimentsManager experimentsManager, ILogger logger)
363
363
  {
364
364
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
365
365
  PackageManager packageManager = new PackageManager(repoRoot, projectPath);
@@ -477,7 +477,7 @@ internal static partial class MSBuildHelper
477
477
  .ToList();
478
478
 
479
479
  // Return as array
480
- Dependency[] candidatePackagesArray = candidatePackages.ToArray();
480
+ var candidatePackagesArray = candidatePackages.ToImmutableArray();
481
481
 
482
482
  var targetFrameworks = new NuGetFramework[] { NuGetFramework.Parse(targetFramework) };
483
483
 
@@ -517,7 +517,7 @@ internal static partial class MSBuildHelper
517
517
  }
518
518
  }
519
519
 
520
- internal static async Task<Dependency[]?> ResolveDependencyConflictsWithBruteForce(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, ExperimentsManager experimentsManager, ILogger logger)
520
+ internal static async Task<ImmutableArray<Dependency>?> ResolveDependencyConflictsWithBruteForce(string repoRoot, string projectPath, string targetFramework, ImmutableArray<Dependency> packages, ExperimentsManager experimentsManager, ILogger logger)
521
521
  {
522
522
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
523
523
  try
@@ -599,7 +599,7 @@ internal static partial class MSBuildHelper
599
599
  {
600
600
  // rebuild candidate dependency list with the relevant versions
601
601
  Dictionary<string, NuGetVersion> packageVersions = candidateSet.ToDictionary(candidateSet => candidateSet.PackageName, candidateSet => candidateSet.PackageVersion);
602
- Dependency[] candidatePackages = packages.Select(p =>
602
+ var candidatePackages = packages.Select(p =>
603
603
  {
604
604
  if (packageVersions.TryGetValue(p.Name, out var version))
605
605
  {
@@ -609,7 +609,7 @@ internal static partial class MSBuildHelper
609
609
 
610
610
  // not the dependency we're looking for, use whatever it already was in this set
611
611
  return p;
612
- }).ToArray();
612
+ }).ToImmutableArray();
613
613
 
614
614
  if (await DependenciesAreCoherentAsync(repoRoot, projectPath, targetFramework, candidatePackages, experimentsManager, logger))
615
615
  {
@@ -748,7 +748,7 @@ internal static partial class MSBuildHelper
748
748
  // empty `Version` attributes will cause the temporary project to not build
749
749
  .Where(p => (p.EvaluationResult is null || p.EvaluationResult.ResultType == EvaluationResultType.Success) && !string.IsNullOrWhiteSpace(p.Version))
750
750
  // 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.
751
- .Select(p => $"<{(usePackageDownload ? "PackageDownload" : "PackageReference")} {(p.IsUpdate ? "Update" : "Include")}=\"{p.Name}\" Version=\"[{p.Version}]\" />"));
751
+ .Select(p => $"<{(usePackageDownload ? "PackageDownload" : "PackageReference")} {(p.IsUpdate ? "Update" : "Include")}=\"{p.Name}\" Version=\"{(p.Version!.Contains("*") ? p.Version : $"[{p.Version}]")}\" />"));
752
752
 
753
753
  var dependencyTargetsImport = importDependencyTargets
754
754
  ? $"""<Import Project="{GetFileFromRuntimeDirectory("DependencyDiscovery.targets")}" />"""
@@ -880,7 +880,7 @@ internal static partial class MSBuildHelper
880
880
  return tfms;
881
881
  }
882
882
 
883
- internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(
883
+ internal static async Task<ImmutableArray<Dependency>> GetAllPackageDependenciesAsync(
884
884
  string repoRoot,
885
885
  string projectPath,
886
886
  string targetFramework,
@@ -895,14 +895,14 @@ internal static partial class MSBuildHelper
895
895
  var topLevelPackagesNames = packages.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
896
896
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, experimentsManager, logger, importDependencyTargets: !experimentsManager.UseDirectDiscovery);
897
897
 
898
- Dependency[] allDependencies;
898
+ ImmutableArray<Dependency> allDependencies;
899
899
  if (experimentsManager.UseDirectDiscovery)
900
900
  {
901
901
  var projectDiscovery = await SdkProjectDiscovery.DiscoverAsync(repoRoot, tempDirectory.FullName, tempProjectPath, experimentsManager, logger);
902
902
  allDependencies = projectDiscovery
903
903
  .Where(p => p.FilePath == Path.GetFileName(tempProjectPath))
904
904
  .FirstOrDefault()
905
- ?.Dependencies.ToArray() ?? [];
905
+ ?.Dependencies.ToImmutableArray() ?? [];
906
906
  }
907
907
  else
908
908
  {
@@ -923,7 +923,7 @@ internal static partial class MSBuildHelper
923
923
  var isTransitive = !topLevelPackagesNames.Contains(PackageName);
924
924
  return new Dependency(PackageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, TargetFrameworks: tfms, IsTransitive: isTransitive);
925
925
  })
926
- .ToArray();
926
+ .ToImmutableArray();
927
927
  }
928
928
  else
929
929
  {
@@ -176,13 +176,13 @@ public partial class DiscoveryWorkerTests
176
176
  public async Task DirectDiscoveryWorksEvenWithTargetsImportsOnlyProvidedByVisualStudio()
177
177
  {
178
178
  await TestDiscoveryAsync(
179
- workspacePath: "",
179
+ workspacePath: "project1/",
180
180
  experimentsManager: new ExperimentsManager() { UseDirectDiscovery = true },
181
181
  packages: [
182
182
  MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net48"),
183
183
  ],
184
184
  files: [
185
- ("project.csproj", """
185
+ ("project1/project1.csproj", """
186
186
  <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
187
187
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
188
188
  <PropertyGroup>
@@ -192,24 +192,38 @@ public partial class DiscoveryWorkerTests
192
192
  <ItemGroup>
193
193
  <None Include="packages.config" />
194
194
  </ItemGroup>
195
+ <ItemGroup>
196
+ <ProjectReference Include="..\project2\project2.csproj" />
197
+ </ItemGroup>
195
198
  <Import Project="$(VSToolsPath)\SomeSubPath\WebApplications\Microsoft.WebApplication.targets" />
196
199
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
197
200
  </Project>
198
201
  """),
199
- ("packages.config", """
202
+ ("project1/packages.config", """
200
203
  <?xml version="1.0" encoding="utf-8"?>
201
204
  <packages>
202
205
  <package id="Some.Package" version="1.0.0" targetFramework="net48" />
203
206
  </packages>
207
+ """),
208
+ ("project2/project2.csproj", """
209
+ <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
210
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
211
+ <PropertyGroup>
212
+ <OutputType>Library</OutputType>
213
+ <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
214
+ </PropertyGroup>
215
+ <Import Project="$(VSToolsPath)\SomeSubPath\WebApplications\Microsoft.WebApplication.targets" />
216
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
217
+ </Project>
204
218
  """)
205
219
  ],
206
220
  expectedResult: new()
207
221
  {
208
- Path = "",
222
+ Path = "project1/",
209
223
  Projects = [
210
224
  new()
211
225
  {
212
- FilePath = "project.csproj",
226
+ FilePath = "project1.csproj",
213
227
  Properties = [],
214
228
  TargetFrameworks = ["net48"],
215
229
  Dependencies = [