diff --git a/src/Fallout.Migrate/Migration.cs b/src/Fallout.Migrate/Migration.cs index 55f2ab655..05f64f422 100644 --- a/src/Fallout.Migrate/Migration.cs +++ b/src/Fallout.Migrate/Migration.cs @@ -27,6 +27,8 @@ internal sealed class Migration(AbsolutePath rootDirectory, bool dryRun, TextWri new VerifyBuildTargetFrameworkStep(), 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 new file mode 100644 index 000000000..0d5f219d2 --- /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, 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\.Frameworks"")(?=[^>\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/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 new file mode 100644 index 000000000..122882353 --- /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(); + } +} 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