From 9fd02e5f83bcc125fca9f2091c4b09853d61f985 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Sat, 29 Aug 2026 17:21:17 +0200 Subject: [PATCH 1/3] Move the explicit nuget framework pin into its own step --- src/Fallout.Migrate/Migration.cs | 1 + .../Steps/RemoveNugetFrameworkPinStep.cs | 39 +++++++ .../RemoveNugetFrameworkPinStepSpecs.cs | 108 ++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs create mode 100644 tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs diff --git a/src/Fallout.Migrate/Migration.cs b/src/Fallout.Migrate/Migration.cs index 55f2ab655..e7258a9ba 100644 --- a/src/Fallout.Migrate/Migration.cs +++ b/src/Fallout.Migrate/Migration.cs @@ -27,6 +27,7 @@ internal sealed class Migration(AbsolutePath rootDirectory, bool dryRun, TextWri new VerifyBuildTargetFrameworkStep(), new ConfirmMigrationStep(), new RewriteCsprojsStep(), + new RemoveNugetFrameworkPinStep(), new BumpDotNetVersionStep(), new RewriteCsFilesStep(), new RewriteBootstrapScriptsStep(), diff --git a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs new file mode 100644 index 000000000..2219263bd --- /dev/null +++ b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs @@ -0,0 +1,39 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Fallout.Migrate.Common; + +namespace Fallout.Migrate.Steps; + +/// +/// Removes explicit NuGet.Framework package pins from project files. The pin was a temporary +/// workaround for .NET SDK 10.0.400 and is no longer needed. +/// +internal sealed class RemoveNugetFrameworkPinStep : IMigrationStep +{ + private static readonly Regex explicitPinPattern = new( + @"^[ \t]*\r\n]*\bInclude=""NuGet\.Framework"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", + RegexOptions.Compiled | RegexOptions.Multiline); + + /// + public Task ExecuteAsync(MigrationContext context, Summary summary) + { + foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "*.csproj")) + { + MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary); + } + + return Task.CompletedTask; + } + + private static RewriteResult Rewrite(string original) + { + var edits = 0; + var content = explicitPinPattern.Replace(original, _ => + { + edits++; + return string.Empty; + }); + + return new RewriteResult(content, edits); + } +} diff --git a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs new file mode 100644 index 000000000..73096ffb1 --- /dev/null +++ b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Fallout.Common.IO; +using Fallout.Migrate.Common; +using Fallout.Migrate.Steps; +using FluentAssertions; +using Xunit; + +namespace Fallout.Migrate.Specs; + +public class RemoveNugetFrameworkPinStepSpecs : IDisposable +{ + private readonly AbsolutePath tempDirectory; + private readonly MigrationContext context; + private readonly Summary summary = new(); + + public RemoveNugetFrameworkPinStepSpecs() + { + tempDirectory = AbsolutePath.Temp("fallout-remove-nuget-framework-pin"); + context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null); + } + + [Fact] + public async Task Explicit_nuget_framework_pin_is_removed() + { + // Arrange + var project = tempDirectory / "Library.csproj"; + project.WriteAllText( + """ + + + + + + + """, eofLineBreak: false); + + // Act + await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary); + + // Assert + project.ReadAllText().Should().Be( + """ + + + + + + """); + + summary.FilesChanged.Should().Be(1); + summary.EditCount.Should().Be(1); + } + + [Fact] + public async Task Nuget_framework_reference_without_version_is_left_unchanged() + { + // Arrange + var project = tempDirectory / "Library.csproj"; + const string input = + """ + + + + + + """; + + project.WriteAllText(input, eofLineBreak: false); + + // Act + await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary); + + // Assert + project.ReadAllText().Should().Be(input); + summary.EditCount.Should().Be(0); + } + + [Fact] + public async Task Missing_nuget_framework_pin_is_left_unchanged() + { + // Arrange + var project = tempDirectory / "build" / "_build.csproj"; + const string input = + """ + + + net10.0 + + + """; + + project.WriteAllText(input, eofLineBreak: false); + + // Act + await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary); + + // Assert + project.ReadAllText().Should().Be(input); + summary.EditCount.Should().Be(0); + } + + public void Dispose() + { + tempDirectory.DeleteDirectory(); + } +} From 44beaee66ebf6d72ff12d3ba604bc678fbb4d545 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Mon, 21 Sep 2026 08:53:16 +0200 Subject: [PATCH 2/3] Also match for other NuGet.* packages with a non-range pin --- .../Steps/RemoveNugetFrameworkPinStep.cs | 6 ++-- .../RemoveNugetFrameworkPinStepSpecs.cs | 36 +++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs index 2219263bd..53562aa77 100644 --- a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs +++ b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs @@ -5,13 +5,13 @@ namespace Fallout.Migrate.Steps; /// -/// Removes explicit NuGet.Framework package pins from project files. The pin was a temporary -/// workaround for .NET SDK 10.0.400 and is no longer needed. +/// Removes explicit NuGet.Framework, NuGet.Protocol, NuGet.Packaging, and +/// NuGet.Resolver package pins from project files. The pins are no longer needed. /// internal sealed class RemoveNugetFrameworkPinStep : IMigrationStep { private static readonly Regex explicitPinPattern = new( - @"^[ \t]*\r\n]*\bInclude=""NuGet\.Framework"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", + @"^[ \t]*\r\n]*\bInclude=""NuGet\.(?:Framework|Protocol|Packaging|Resolver)"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", RegexOptions.Compiled | RegexOptions.Multiline); /// diff --git a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs index 73096ffb1..ab22a0b98 100644 --- a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs @@ -31,7 +31,7 @@ public async Task Explicit_nuget_framework_pin_is_removed() - + """, eofLineBreak: false); @@ -44,7 +44,7 @@ public async Task Explicit_nuget_framework_pin_is_removed() """ - + """); @@ -53,6 +53,38 @@ public async Task Explicit_nuget_framework_pin_is_removed() summary.EditCount.Should().Be(1); } + [Theory] + [InlineData("NuGet.Protocol")] + [InlineData("NuGet.Packaging")] + [InlineData("NuGet.Resolver")] + public async Task Explicit_related_nuget_pin_is_removed(string packageName) + { + // Arrange + var project = tempDirectory / "Library.csproj"; + project.WriteAllText( + $""" + + + + + + """, eofLineBreak: false); + + // Act + await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary); + + // Assert + project.ReadAllText().Should().Be( + """ + + + + + """); + summary.FilesChanged.Should().Be(1); + summary.EditCount.Should().Be(1); + } + [Fact] public async Task Nuget_framework_reference_without_version_is_left_unchanged() { From a7f2c17ba48849b3475339e3658d88cfe0b066d2 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Mon, 21 Sep 2026 11:53:58 +0200 Subject: [PATCH 3/3] Add another migration step to remove explicit other `NuGet.*` packages This only runs for v11 or later --- src/Fallout.Migrate/Migration.cs | 1 + .../Steps/RemoveNugetFrameworkPinStep.cs | 2 +- .../Steps/RemoveRelatedNugetPinsStep.cs | 56 +++++++++++ .../RemoveNugetFrameworkPinStepSpecs.cs | 36 +------ .../RemoveRelatedNugetPinsStepSpecs.cs | 94 +++++++++++++++++++ 5 files changed, 154 insertions(+), 35 deletions(-) create mode 100644 src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs create mode 100644 tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs diff --git a/src/Fallout.Migrate/Migration.cs b/src/Fallout.Migrate/Migration.cs index e7258a9ba..05f64f422 100644 --- a/src/Fallout.Migrate/Migration.cs +++ b/src/Fallout.Migrate/Migration.cs @@ -28,6 +28,7 @@ internal sealed class Migration(AbsolutePath rootDirectory, bool dryRun, TextWri new ConfirmMigrationStep(), new RewriteCsprojsStep(), new RemoveNugetFrameworkPinStep(), + new RemoveRelatedNugetPinsStep(), new BumpDotNetVersionStep(), new RewriteCsFilesStep(), new RewriteBootstrapScriptsStep(), diff --git a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs index 53562aa77..0d5f219d2 100644 --- a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs +++ b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs @@ -11,7 +11,7 @@ namespace Fallout.Migrate.Steps; internal sealed class RemoveNugetFrameworkPinStep : IMigrationStep { private static readonly Regex explicitPinPattern = new( - @"^[ \t]*\r\n]*\bInclude=""NuGet\.(?:Framework|Protocol|Packaging|Resolver)"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", + @"^[ \t]*\r\n]*\bInclude=""NuGet\.Frameworks"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", RegexOptions.Compiled | RegexOptions.Multiline); /// diff --git a/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs b/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs new file mode 100644 index 000000000..fa1381551 --- /dev/null +++ b/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs @@ -0,0 +1,56 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Fallout.Migrate.Common; + +namespace Fallout.Migrate.Steps; + +/// +/// Removes explicit NuGet.Protocol, NuGet.Packaging, and NuGet.Resolver +/// package pins from project files when migrating to Fallout 11 or later. +/// +internal sealed class RemoveRelatedNugetPinsStep : IMigrationStep +{ + private const int MinimumFalloutMajor = 11; + + private static readonly Regex explicitPinPattern = new( + @"^[ \t]*\r\n]*\bInclude=""NuGet\.(?:Protocol|Packaging|Resolver)"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", + RegexOptions.Compiled | RegexOptions.Multiline); + + /// + public Task ExecuteAsync(MigrationContext context, Summary summary) + { + if (!RunsFor(context.FalloutVersion)) + { + return Task.CompletedTask; + } + + foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "*.csproj")) + { + MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary); + } + + return Task.CompletedTask; + } + + private static bool RunsFor(string version) + { + int separatorIndex = version?.IndexOf('.') ?? -1; + string majorSegment = separatorIndex == -1 ? version : version[..separatorIndex]; + + return int.TryParse(majorSegment, NumberStyles.None, CultureInfo.InvariantCulture, out int major) && + major >= MinimumFalloutMajor; + } + + private static RewriteResult Rewrite(string original) + { + var edits = 0; + var content = explicitPinPattern.Replace(original, _ => + { + edits++; + return string.Empty; + }); + + return new RewriteResult(content, edits); + } +} \ No newline at end of file diff --git a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs index ab22a0b98..122882353 100644 --- a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs @@ -30,7 +30,7 @@ public async Task Explicit_nuget_framework_pin_is_removed() """ - + @@ -53,38 +53,6 @@ public async Task Explicit_nuget_framework_pin_is_removed() summary.EditCount.Should().Be(1); } - [Theory] - [InlineData("NuGet.Protocol")] - [InlineData("NuGet.Packaging")] - [InlineData("NuGet.Resolver")] - public async Task Explicit_related_nuget_pin_is_removed(string packageName) - { - // Arrange - var project = tempDirectory / "Library.csproj"; - project.WriteAllText( - $""" - - - - - - """, eofLineBreak: false); - - // Act - await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary); - - // Assert - project.ReadAllText().Should().Be( - """ - - - - - """); - summary.FilesChanged.Should().Be(1); - summary.EditCount.Should().Be(1); - } - [Fact] public async Task Nuget_framework_reference_without_version_is_left_unchanged() { @@ -94,7 +62,7 @@ public async Task Nuget_framework_reference_without_version_is_left_unchanged() """ - + """; diff --git a/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs new file mode 100644 index 000000000..f98b728d5 --- /dev/null +++ b/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Fallout.Common.IO; +using Fallout.Migrate.Common; +using Fallout.Migrate.Steps; +using FluentAssertions; +using Xunit; + +namespace Fallout.Migrate.Specs; + +public class RemoveRelatedNugetPinsStepSpecs : IDisposable +{ + private readonly AbsolutePath tempDirectory; + private readonly Summary summary = new(); + + public RemoveRelatedNugetPinsStepSpecs() + { + tempDirectory = AbsolutePath.Temp("fallout-remove-related-nuget-pins"); + } + + [Theory] + [InlineData("NuGet.Protocol", "11.0.0")] + [InlineData("NuGet.Packaging", "11.0.0-preview.1")] + [InlineData("NuGet.Resolver", "12.0.0")] + public async Task Explicit_related_nuget_pin_is_removed_for_v11_or_later( + string packageName, + string falloutVersion) + { + // Arrange + var project = tempDirectory / "Library.csproj"; + project.WriteAllText( + $""" + + + + + + """, eofLineBreak: false); + var context = CreateContext(falloutVersion); + + // Act + await new RemoveRelatedNugetPinsStep().ExecuteAsync(context, summary); + + // Assert + project.ReadAllText().Should().Be( + """ + + + + + """); + summary.FilesChanged.Should().Be(1); + summary.EditCount.Should().Be(1); + } + + [Theory] + [InlineData("10.9.0")] + [InlineData(null)] + public async Task Explicit_related_nuget_pin_is_left_unchanged_before_v11(string falloutVersion) + { + // Arrange + var project = tempDirectory / "Library.csproj"; + const string input = + """ + + + + + + """; + + project.WriteAllText(input, eofLineBreak: false); + var context = CreateContext(falloutVersion); + + // Act + await new RemoveRelatedNugetPinsStep().ExecuteAsync(context, summary); + + // Assert + project.ReadAllText().Should().Be(input); + summary.EditCount.Should().Be(0); + } + + private MigrationContext CreateContext(string falloutVersion) => + new(tempDirectory, dryRun: false, TextWriter.Null) + { + FalloutVersion = falloutVersion + }; + + public void Dispose() + { + tempDirectory.DeleteDirectory(); + } +} \ No newline at end of file