dependabot-nuget 0.301.1 → 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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/Directory.Packages.props +5 -5
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyDiscovery.props +4 -1
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyDiscovery.targets +19 -4
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyDiscoveryTargetingPacks.props +10 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +20 -17
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +179 -28
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +3 -0
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +15 -6
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +6 -4
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +1 -0
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +8 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +7 -4
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/LockFileUpdater.cs +2 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +257 -37
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +13 -4
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/{WebApplicationTargetsConditionPatcher.cs → SpecialImportsConditionPatcher.cs} +18 -11
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationBase.cs +209 -0
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationResult.cs +3 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +79 -24
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs +26 -11
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +48 -22
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +54 -0
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +68 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +94 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/FrameworkCompatibilityServiceFacts.cs +1 -1
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +24 -6
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackageReferenceUpdaterTests.cs +177 -0
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/SpecialFilePatcherTests.cs +99 -0
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateOperationBaseTests.cs +130 -0
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +5 -0
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +71 -5
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackageReference.cs +125 -3
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +23 -0
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +145 -147
  36. data/lib/dependabot/nuget/file_parser.rb +22 -19
  37. metadata +13 -8
@@ -2,43 +2,50 @@ using Microsoft.Language.Xml;
2
2
 
3
3
  namespace NuGetUpdater.Core.Updater
4
4
  {
5
- internal class WebApplicationTargetsConditionPatcher : IDisposable
5
+ internal class SpecialImportsConditionPatcher : IDisposable
6
6
  {
7
- private string? _capturedCondition;
7
+ private readonly List<string?> _capturedConditions = new List<string?>();
8
8
  private readonly XmlFilePreAndPostProcessor _processor;
9
9
 
10
- public WebApplicationTargetsConditionPatcher(string projectFilePath)
10
+ private readonly HashSet<string> ImportedFilesToIgnore = new(StringComparer.OrdinalIgnoreCase)
11
+ {
12
+ "Microsoft.TextTemplating.targets",
13
+ "Microsoft.WebApplication.targets"
14
+ };
15
+
16
+ public SpecialImportsConditionPatcher(string projectFilePath)
11
17
  {
12
18
  _processor = new XmlFilePreAndPostProcessor(
13
19
  getContent: () => File.ReadAllText(projectFilePath),
14
20
  setContent: s => File.WriteAllText(projectFilePath, s),
15
21
  nodeFinder: doc => doc.Descendants()
16
22
  .Where(e => e.Name == "Import")
17
- .FirstOrDefault(e =>
23
+ .Where(e =>
18
24
  {
19
25
  var projectPath = e.GetAttributeValue("Project");
20
26
  if (projectPath is not null)
21
27
  {
22
28
  var projectFileName = Path.GetFileName(projectPath.NormalizePathToUnix());
23
- return projectFileName.Equals("Microsoft.WebApplication.targets", StringComparison.OrdinalIgnoreCase);
29
+ return ImportedFilesToIgnore.Contains(projectFileName);
24
30
  }
25
31
 
26
32
  return false;
27
33
  })
28
- as XmlNodeSyntax,
29
- preProcessor: n =>
34
+ .Cast<XmlNodeSyntax>(),
35
+ preProcessor: (i, n) =>
30
36
  {
31
37
  var element = (IXmlElementSyntax)n;
32
- _capturedCondition = element.GetAttributeValue("Condition");
38
+ _capturedConditions.Add(element.GetAttributeValue("Condition"));
33
39
  return (XmlNodeSyntax)element.RemoveAttributeByName("Condition").WithAttribute("Condition", "false");
34
40
  },
35
- postProcessor: n =>
41
+ postProcessor: (i, n) =>
36
42
  {
37
43
  var element = (IXmlElementSyntax)n;
38
44
  var newElement = element.RemoveAttributeByName("Condition");
39
- if (_capturedCondition is not null)
45
+ var capturedCondition = _capturedConditions[i];
46
+ if (capturedCondition is not null)
40
47
  {
41
- newElement = newElement.WithAttribute("Condition", _capturedCondition);
48
+ newElement = newElement.WithAttribute("Condition", capturedCondition);
42
49
  }
43
50
 
44
51
  return (XmlNodeSyntax)newElement;
@@ -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
  }
@@ -1,3 +1,5 @@
1
+ using System.Collections.Immutable;
2
+
1
3
  using Microsoft.Language.Xml;
2
4
 
3
5
  namespace NuGetUpdater.Core.Updater
@@ -6,11 +8,11 @@ namespace NuGetUpdater.Core.Updater
6
8
  {
7
9
  public Func<string> GetContent { get; }
8
10
  public Action<string> SetContent { get; }
9
- public Func<XmlDocumentSyntax, XmlNodeSyntax?> NodeFinder { get; }
10
- public Func<XmlNodeSyntax, XmlNodeSyntax> PreProcessor { get; }
11
- public Func<XmlNodeSyntax, XmlNodeSyntax> PostProcessor { get; }
11
+ public Func<XmlDocumentSyntax, IEnumerable<XmlNodeSyntax>> NodeFinder { get; }
12
+ public Func<int, XmlNodeSyntax, XmlNodeSyntax> PreProcessor { get; }
13
+ public Func<int, XmlNodeSyntax, XmlNodeSyntax> PostProcessor { get; }
12
14
 
13
- public XmlFilePreAndPostProcessor(Func<string> getContent, Action<string> setContent, Func<XmlDocumentSyntax, XmlNodeSyntax?> nodeFinder, Func<XmlNodeSyntax, XmlNodeSyntax> preProcessor, Func<XmlNodeSyntax, XmlNodeSyntax> postProcessor)
15
+ public XmlFilePreAndPostProcessor(Func<string> getContent, Action<string> setContent, Func<XmlDocumentSyntax, IEnumerable<XmlNodeSyntax>> nodeFinder, Func<int, XmlNodeSyntax, XmlNodeSyntax> preProcessor, Func<int, XmlNodeSyntax, XmlNodeSyntax> postProcessor)
14
16
  {
15
17
  GetContent = getContent;
16
18
  SetContent = setContent;
@@ -29,7 +31,7 @@ namespace NuGetUpdater.Core.Updater
29
31
 
30
32
  private void PostProcess() => RunProcessor(PostProcessor);
31
33
 
32
- private void RunProcessor(Func<XmlNodeSyntax, XmlNodeSyntax> processor)
34
+ private void RunProcessor(Func<int, XmlNodeSyntax, XmlNodeSyntax> processor)
33
35
  {
34
36
  var content = GetContent();
35
37
  var xml = Parser.ParseText(content);
@@ -38,15 +40,28 @@ namespace NuGetUpdater.Core.Updater
38
40
  return;
39
41
  }
40
42
 
41
- var node = NodeFinder(xml);
42
- if (node is null)
43
+ var offset = 0;
44
+ var nodes = NodeFinder(xml).ToImmutableArray();
45
+ for (int i = 0; i < nodes.Length; i++)
43
46
  {
44
- return;
47
+ // modify the node...
48
+ var node = nodes[i];
49
+ var replacementElement = processor(i, node);
50
+
51
+ // ...however, the XML structure we're using is immutable and calling `.ReplaceNode()` below will fail because the nodes are no longer equal
52
+ // find the equivalent node by offset, accounting for any changes in length
53
+ var candidateEquivalentNodes = xml.DescendantNodes().OfType<XmlNodeSyntax>().ToArray();
54
+ var equivalentNode = candidateEquivalentNodes.First(n => n.Start == node.Start + offset);
55
+
56
+ // do the actual replacement
57
+ xml = xml.ReplaceNode(equivalentNode, replacementElement);
58
+
59
+ // update our offset
60
+ var thisNodeOffset = replacementElement.ToFullString().Length - node.ToFullString().Length;
61
+ offset += thisNodeOffset;
45
62
  }
46
63
 
47
- var replacementElement = processor(node);
48
- var replacementXml = xml.ReplaceNode(node, replacementElement);
49
- var replacementString = replacementXml.ToFullString();
64
+ var replacementString = xml.ToFullString();
50
65
  SetContent(replacementString);
51
66
  }
52
67
  }