From f2a6455ec97b4db56dee9257c4b287fd3b5e0db6 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Sat, 29 Aug 2026 17:21:17 +0200 Subject: [PATCH 1/8] Move the explicit nuget framework pin into its own step Soli Deo Gloria --- 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 42bd53269198838fb94d5abec809be14d35443d6 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Mon, 21 Sep 2026 08:53:16 +0200 Subject: [PATCH 2/8] Also match for other NuGet.* packages with a non-range pin Soli Deo Gloria --- .../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 7ac195440d0385b168b0c6f28c14e490e5ee037e Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Mon, 21 Sep 2026 11:53:58 +0200 Subject: [PATCH 3/8] Add another migration step to remove explicit other `NuGet.*` packages This only runs for v11 or later Soli Deo Gloria --- 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 From 646e005c1ef41a2c021e3391f8f44e3b370653a0 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Thu, 15 Oct 2026 13:14:18 +0200 Subject: [PATCH 4/8] Add detailed regex for explicit NuGet pin pattern Soli Deo Gloria --- .../Steps/RemoveNugetFrameworkPinStep.cs | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs index 0d5f219d2..e66fd3b45 100644 --- a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs +++ b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs @@ -10,6 +10,136 @@ namespace Fallout.Migrate.Steps; /// internal sealed class RemoveNugetFrameworkPinStep : IMigrationStep { + /// + /// Matches a single-line, self-closing NuGet package reference for + /// NuGet.Frameworks that explicitly specifies a version. + /// + /// + /// + /// This regular expression is intended to identify explicitly version-pinned + /// NuGet.Frameworks package references in XML-based MSBuild project + /// files, such as .csproj, .props, or .targets. + /// + /// + /// + /// A match must satisfy all of the following conditions: + /// + /// + /// + /// + /// The element begins at the start of a line, optionally preceded + /// by spaces or tab characters. + /// + /// + /// + /// + /// The element starts with the exact, case-sensitive text + /// <PackageReference, followed by at least one + /// whitespace character. + /// + /// + /// + /// + /// The element contains the exact attribute + /// Include="NuGet.Frameworks". + /// + /// + /// + /// + /// The element contains a Version attribute whose value + /// consists of one or more characters other than a double quote. + /// + /// + /// + /// + /// Both required attributes occur on the same line, before + /// the first greater-than character. + /// Their order does not matter. + /// + /// + /// + /// + /// The element ends with the exact sequence />, + /// optionally followed by spaces or tabs. + /// + /// + /// + /// + /// + /// The match also consumes an optional line ending (\n or + /// \r\n) immediately following the element. + /// + /// + /// + /// The pattern uses two positive lookaheads to verify the presence + /// of the required attributes without enforcing their order. + /// + /// + /// + /// Matching is case-sensitive. Only self-closing, single-line elements + /// are supported. This is a textual pattern, not a full XML parser. + /// + /// + /// + /// Examples that match: + /// + /// + /// <PackageReference Include="NuGet.Frameworks" Version="6.0.0" /> + /// <PackageReference Version="6.0.0" Include="NuGet.Frameworks"/> + /// <PackageReference Include="NuGet.Frameworks" Version="7.0.0" /> + /// <PackageReference Include="NuGet.Frameworks" Version="[6.0.0]" /> + /// + /// + /// + /// Examples that do not match: + /// + /// + /// <PackageReference Include="Other.Package" Version="6.0.0" /> + /// <PackageReference Include="NuGet.Frameworks" /> + /// <PackageReference Include="NuGet.Frameworks" Version="6.0.0"> + /// <PackageReference Include="NuGet.Frameworks" + /// Version="6.0.0" /> + /// + /// + /// + /// Important limitations: + /// + /// + /// + /// + /// Does not support multiline package reference elements. + /// + /// + /// + /// + /// Does not match references using single-quoted attributes. + /// + /// + /// + /// + /// Does not match versions specified using child + /// <Version> elements. + /// + /// + /// + /// + /// Does not validate that a version is a valid NuGet version. + /// + /// + /// + /// + /// Does not require the matched element to be the only + /// content on the line after the optional trailing whitespace. + /// + /// + /// + /// + /// Does not validate XML syntax or distinguish XML markup + /// from text inside comments. + /// + /// + /// + /// private static readonly Regex explicitPinPattern = new( @"^[ \t]*\r\n]*\bInclude=""NuGet\.Frameworks"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", RegexOptions.Compiled | RegexOptions.Multiline); From 6f3dba9ab4abb6b77bdc3f594d5640afb9c23dbb Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Thu, 15 Oct 2026 13:15:45 +0200 Subject: [PATCH 5/8] Update the RemoveNugetFrameworkPinStep summary to fit what it does Soli Deo Gloria --- src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs index e66fd3b45..c563b2bdf 100644 --- a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs +++ b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs @@ -5,8 +5,7 @@ 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. +/// Removes explicit NuGet.Frameworks package pins from project files. The pins are no longer needed. /// internal sealed class RemoveNugetFrameworkPinStep : IMigrationStep { From 963b7690a84baf22748736891052ab275aae2a82 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Thu, 15 Oct 2026 13:45:03 +0200 Subject: [PATCH 6/8] Update some method docs Soli Deo Gloria --- .../Steps/RemoveNugetFrameworkPinStep.cs | 141 ++++++------------ .../Steps/RemoveRelatedNugetPinsStep.cs | 105 ++++++++++++- .../RemoveNugetFrameworkPinStepSpecs.cs | 31 +++- 3 files changed, 173 insertions(+), 104 deletions(-) diff --git a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs index c563b2bdf..7b47276be 100644 --- a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs +++ b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs @@ -1,3 +1,4 @@ +using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using Fallout.Migrate.Common; @@ -10,143 +11,87 @@ namespace Fallout.Migrate.Steps; internal sealed class RemoveNugetFrameworkPinStep : IMigrationStep { /// - /// Matches a single-line, self-closing NuGet package reference for - /// NuGet.Frameworks that explicitly specifies a version. + /// Matches an entire single-line, self-closing + /// PackageReference element for NuGet.Frameworks + /// that explicitly declares a non-empty Version attribute, + /// including its indentation and trailing line ending. /// /// /// - /// This regular expression is intended to identify explicitly version-pinned - /// NuGet.Frameworks package references in XML-based MSBuild project - /// files, such as .csproj, .props, or .targets. + /// Intended for removing explicitly versioned + /// NuGet.Frameworks package references from MSBuild XML + /// files such as .csproj, .props, and .targets. /// - /// /// - /// A match must satisfy all of the following conditions: + /// The Include and Version attributes may appear + /// in any order. Additional attributes, such as + /// PrivateAssets, are allowed before, between, or after + /// the required attributes. /// - /// - /// - /// - /// The element begins at the start of a line, optionally preceded - /// by spaces or tab characters. - /// - /// + /// + /// Matching requirements: + /// + /// /// /// - /// The element starts with the exact, case-sensitive text - /// <PackageReference, followed by at least one - /// whitespace character. + /// The element must begin on a line containing only optional + /// leading spaces or tabs. /// /// /// /// - /// The element contains the exact attribute + /// The element must contain /// Include="NuGet.Frameworks". /// /// /// /// - /// The element contains a Version attribute whose value - /// consists of one or more characters other than a double quote. + /// The element must contain a non-empty + /// Version="..." attribute. /// /// /// /// - /// Both required attributes occur on the same line, before - /// the first greater-than character. - /// Their order does not matter. - /// - /// - /// - /// - /// The element ends with the exact sequence />, - /// optionally followed by spaces or tabs. + /// The element must be self-closing and contained on one line. /// /// /// - /// - /// - /// The match also consumes an optional line ending (\n or - /// \r\n) immediately following the element. - /// - /// /// - /// The pattern uses two positive lookaheads to verify the presence - /// of the required attributes without enforcing their order. + /// The expression consumes the entire matching line, including + /// its indentation, optional trailing whitespace, and line + /// ending (if present). Replacing matches with an empty string + /// therefore removes the package reference without leaving + /// an empty line. /// - /// /// - /// Matching is case-sensitive. Only self-closing, single-line elements - /// are supported. This is a textual pattern, not a full XML parser. - /// - /// - /// - /// Examples that match: - /// - /// - /// <PackageReference Include="NuGet.Frameworks" Version="6.0.0" /> - /// <PackageReference Version="6.0.0" Include="NuGet.Frameworks"/> - /// <PackageReference Include="NuGet.Frameworks" Version="7.0.0" /> - /// <PackageReference Include="NuGet.Frameworks" Version="[6.0.0]" /> - /// - /// - /// - /// Examples that do not match: + /// Example: /// /// - /// <PackageReference Include="Other.Package" Version="6.0.0" /> - /// <PackageReference Include="NuGet.Frameworks" /> - /// <PackageReference Include="NuGet.Frameworks" Version="6.0.0"> - /// <PackageReference Include="NuGet.Frameworks" - /// Version="6.0.0" /> + /// <PackageReference Version="7.9.0" PrivateAssets="all" Include="NuGet.Frameworks" /> /// - /// /// - /// Important limitations: + /// Limitations: + /// The expression is case-sensitive, supports double-quoted + /// attributes only, and does not match multiline elements. + /// It does not validate XML structure or NuGet version syntax + /// and may match package references inside XML comments. /// - /// - /// - /// - /// Does not support multiline package reference elements. - /// - /// - /// - /// - /// Does not match references using single-quoted attributes. - /// - /// - /// - /// - /// Does not match versions specified using child - /// <Version> elements. - /// - /// - /// - /// - /// Does not validate that a version is a valid NuGet version. - /// - /// - /// - /// - /// Does not require the matched element to be the only - /// content on the line after the optional trailing whitespace. - /// - /// - /// - /// - /// Does not validate XML syntax or distinguish XML markup - /// from text inside comments. - /// - /// - /// /// private static readonly Regex explicitPinPattern = new( - @"^[ \t]*\r\n]*\bInclude=""NuGet\.Frameworks"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", + @"^[ \t]*]*[ \t]Include=""NuGet\.Frameworks"")(?=[^\r\n>]*[ \t]Version=""[^""]+"")[^\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")) + Version.TryParse(context.FalloutVersion, out var versionGate); + + if (versionGate < Version.Parse("10.5.0")) + { + return Task.CompletedTask; + } + + foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "_build.csproj")) { MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary); } diff --git a/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs b/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs index fa1381551..df42dd86a 100644 --- a/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs +++ b/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs @@ -11,21 +11,103 @@ namespace Fallout.Migrate.Steps; /// internal sealed class RemoveRelatedNugetPinsStep : IMigrationStep { + /// + /// The minimum Fallout major version that requires this step. On migrations below that version ths step is skipped. + /// private const int MinimumFalloutMajor = 11; + /// + /// Matches an entire single-line, self-closing + /// PackageReference element for NuGet.Protocol, + /// NuGet.Packaging, or NuGet.Resolver that explicitly + /// declares a non-empty Version attribute, including its + /// indentation and trailing line ending. + /// + /// + /// + /// Intended for removing explicitly versioned NuGet package + /// references from MSBuild XML files such as .csproj, + /// .props, and .targets. + /// + /// + /// The Include and Version attributes may appear + /// in any order. Additional attributes, such as + /// PrivateAssets, are allowed before, between, or after + /// the required attributes. + /// + /// + /// Matching requirements: + /// + /// + /// + /// + /// The element must begin on a line containing only optional + /// leading spaces or tabs. + /// + /// + /// + /// + /// The element must contain an Include attribute + /// identifying one of the following packages: + /// NuGet.Protocol, NuGet.Packaging, + /// or NuGet.Resolver. + /// + /// + /// + /// + /// The element must contain a non-empty + /// Version="..." attribute. + /// + /// + /// + /// + /// The element must be self-closing and contained on one line. + /// + /// + /// + /// + /// Two positive lookaheads independently verify the package name + /// and version attribute. Their positions allow either attribute + /// to appear first, with arbitrary additional attributes between + /// them. + /// + /// + /// The expression consumes the entire matching line, including + /// its indentation, optional trailing whitespace, and line + /// ending (if present). Replacing matches with an empty string + /// therefore removes the package reference without leaving + /// an empty line. + /// + /// + /// Matching examples: + /// + /// + /// <PackageReference Include="NuGet.Protocol" Version="6.0.0" /> + /// <PackageReference Version="7.9.0" Include="NuGet.Packaging" /> + /// <PackageReference Version="7.9.0" PrivateAssets="all" Include="NuGet.Resolver" /> + /// <PackageReference Include="NuGet.Protocol" PrivateAssets="all" Version="6.0.0" /> + /// + /// + /// Limitations: + /// Matching is case-sensitive and supports double-quoted + /// attributes only. Multiline elements are not supported. + /// The expression does not validate XML structure or NuGet + /// version syntax and may match elements inside XML comments. + /// + /// private static readonly Regex explicitPinPattern = new( - @"^[ \t]*\r\n]*\bInclude=""NuGet\.(?:Protocol|Packaging|Resolver)"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?", + @"^[ \t]*]*[ \t]Include=""NuGet\.(?:Protocol|Packaging|Resolver)"")(?=[^\r\n>]*[ \t]Version=""[^""]+"")[^\r\n>]*?/>[ \t]*(?:\r?\n|$)", RegexOptions.Compiled | RegexOptions.Multiline); /// public Task ExecuteAsync(MigrationContext context, Summary summary) { - if (!RunsFor(context.FalloutVersion)) + if (!RunsForMinimumRequiredVersion(context.FalloutVersion)) { return Task.CompletedTask; } - foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "*.csproj")) + foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "_build.csproj")) { MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary); } @@ -33,7 +115,20 @@ public Task ExecuteAsync(MigrationContext context, Summary summary) return Task.CompletedTask; } - private static bool RunsFor(string version) + /// + /// if the major version can be parsed as a + /// non-negative integer and is greater than or equal to + /// ; otherwise, . + /// Returns for , + /// empty, or unparseable version strings. + /// + /// + /// The major version is parsed using + /// and . + /// Minor, patch, and subsequent version segments are ignored and + /// are not validated. + /// + private static bool RunsForMinimumRequiredVersion(string version) { int separatorIndex = version?.IndexOf('.') ?? -1; string majorSegment = separatorIndex == -1 ? version : version[..separatorIndex]; @@ -53,4 +148,4 @@ private static RewriteResult Rewrite(string original) 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 122882353..473e7261f 100644 --- a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs @@ -11,6 +11,8 @@ namespace Fallout.Migrate.Specs; public class RemoveNugetFrameworkPinStepSpecs : IDisposable { + private const string TestFalloutVersion = "10.5.0"; + private readonly AbsolutePath tempDirectory; private readonly MigrationContext context; private readonly Summary summary = new(); @@ -18,7 +20,10 @@ public class RemoveNugetFrameworkPinStepSpecs : IDisposable public RemoveNugetFrameworkPinStepSpecs() { tempDirectory = AbsolutePath.Temp("fallout-remove-nuget-framework-pin"); - context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null); + context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null) + { + FalloutVersion = TestFalloutVersion + }; } [Fact] @@ -76,6 +81,30 @@ public async Task Nuget_framework_reference_without_version_is_left_unchanged() project.ReadAllText().Should().Be(input); summary.EditCount.Should().Be(0); } + + [Fact] + public async Task Nuget_framework_reference_is_left_unchanged_in_non_build_projects() + { + // 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() From f9eae200fdeb49a8ffedf09c84b7118844b05ca7 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Thu, 15 Oct 2026 13:49:23 +0200 Subject: [PATCH 7/8] Add missing test case: prior 10.5.0 migration Soli Deo Gloria --- .../RemoveNugetFrameworkPinStepSpecs.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs index 473e7261f..164a8c922 100644 --- a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs @@ -58,6 +58,34 @@ public async Task Explicit_nuget_framework_pin_is_removed() summary.EditCount.Should().Be(1); } + [Fact] + public async Task Explicit_nuget_framework_pin_is_left_alone_for_version_prior_10_5() + { + // Arrange + var project = tempDirectory / "_build.csproj"; + var input = + """ + + + + + + + """; + project.WriteAllText(input, eofLineBreak: false); + context.FalloutVersion = "10.4.0"; + + // Act + await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary); + + // Assert + project.ReadAllText().Should().Be(input); + + summary.FilesChanged.Should().Be(0); + summary.EditCount.Should().Be(0); + } + + [Fact] public async Task Nuget_framework_reference_without_version_is_left_unchanged() { @@ -81,7 +109,7 @@ public async Task Nuget_framework_reference_without_version_is_left_unchanged() project.ReadAllText().Should().Be(input); summary.EditCount.Should().Be(0); } - + [Fact] public async Task Nuget_framework_reference_is_left_unchanged_in_non_build_projects() { From b173ded489e7e66db659a19852da026cf93de694 Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger | alu-one Date: Thu, 15 Oct 2026 15:11:11 +0200 Subject: [PATCH 8/8] Fix wrong tests Soli Deo Gloria --- .../Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs | 4 ++-- .../Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs index 164a8c922..c631eacdd 100644 --- a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs @@ -30,7 +30,7 @@ public RemoveNugetFrameworkPinStepSpecs() public async Task Explicit_nuget_framework_pin_is_removed() { // Arrange - var project = tempDirectory / "Library.csproj"; + var project = tempDirectory / "_build.csproj"; project.WriteAllText( """ @@ -90,7 +90,7 @@ public async Task Explicit_nuget_framework_pin_is_left_alone_for_version_prior_1 public async Task Nuget_framework_reference_without_version_is_left_unchanged() { // Arrange - var project = tempDirectory / "Library.csproj"; + var project = tempDirectory / "_build.csproj"; const string input = """ diff --git a/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs index f98b728d5..85aa03941 100644 --- a/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs @@ -28,7 +28,7 @@ public async Task Explicit_related_nuget_pin_is_removed_for_v11_or_later( string falloutVersion) { // Arrange - var project = tempDirectory / "Library.csproj"; + var project = tempDirectory / "_build.csproj"; project.WriteAllText( $""" @@ -60,7 +60,7 @@ public async Task Explicit_related_nuget_pin_is_removed_for_v11_or_later( public async Task Explicit_related_nuget_pin_is_left_unchanged_before_v11(string falloutVersion) { // Arrange - var project = tempDirectory / "Library.csproj"; + var project = tempDirectory / "_build.csproj"; const string input = """