Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/Fallout.Migrate/Common/TargetFrameworkMonikers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,21 @@ public static bool IsOlderThanMinimumSupported(string moniker, int minimumMajor)
int major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
return major < minimumMajor;
}

/// <summary>
/// Returns <c>true</c> for modern .NET monikers (<c>net5.0</c> and later).
/// .NET Framework (<c>net48</c>, <c>net472</c>) and .NET Standard are <c>false</c>.
/// </summary>
/// <param name="moniker">A single target framework moniker, e.g. <c>net10.0</c> or <c>net48</c>.</param>
public static bool TargetsModernDotNet(string moniker)
{
Match match = modernMonikerPattern.Match(moniker.Trim());
if (!match.Success)
{
return false;
}

int major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
return major >= 5;
}
}
95 changes: 90 additions & 5 deletions src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
Expand All @@ -10,7 +11,9 @@ namespace Fallout.Migrate.Steps;
/// Rewrites every <c>*.csproj</c> file under the repository root: <c>Nuke.*</c> package/project
/// references become <c>Fallout.*</c> (pinning the current Fallout version where an inline
/// <c>Version</c> attribute was present), <c>Nuke*</c> MSBuild properties are renamed to
/// <c>Fallout*</c>, and stale explicit <c>System.Security.Cryptography.Xml</c> pins are stripped.
/// <c>Fallout*</c>, stale explicit <c>System.Security.Cryptography.Xml</c> pins are stripped,
/// and a temporary <c>NuGet.Framework</c> 7.9.0 pin is added on <c>_build.csproj</c> when that
/// project targets modern .NET (stripped once the marker major is reached).
/// </summary>
internal sealed class RewriteCsprojsStep : IMigrationStep
{
Expand Down Expand Up @@ -74,6 +77,16 @@ internal sealed class RewriteCsprojsStep : IMigrationStep
@"^[ \t]*<PackageReference\s+Include=""System\.Security\.Cryptography\.Xml""[^/]*/>[ \t]*\r?\n?",
RegexOptions.Compiled | RegexOptions.Multiline);

// Temporary pin for .NET SDK 10.0.400. The marker names the Fallout major that drops it
// (the next major after MigrationContext.FalloutVersion).
private static readonly Regex nugetFrameworkPinPattern = new(
@"\r?\n[ \t]*<!-- fallout-migrate:delete-at-v(?<major>\d+):start -->[\s\S]*?<!-- fallout-migrate:delete-at-v\k<major>:end -->\r?\n?",
RegexOptions.Compiled);

private static readonly Regex targetFrameworkElementPattern = new(
@"<TargetFrameworks?>(?<value>[^<]+)</TargetFrameworks?>",
RegexOptions.Compiled);

/// <inheritdoc />
public Task ExecuteAsync(MigrationContext context, Summary summary)
{
Expand All @@ -82,7 +95,10 @@ public Task ExecuteAsync(MigrationContext context, Summary summary)
MigrationFileOperations.ApplyRewrite(
context,
path,
content => Rewrite(content, context.FalloutVersion),
content => Rewrite(
content,
context.FalloutVersion,
isBuildProject: path.Name.Equals("_build.csproj", StringComparison.OrdinalIgnoreCase)),
summary);
}

Expand All @@ -91,12 +107,14 @@ public Task ExecuteAsync(MigrationContext context, Summary summary)

/// <summary>
/// Rewrites <paramref name="original"/> content, replacing <c>Nuke.*</c> references and MSBuild
/// properties with their <c>Fallout.*</c> equivalents and stripping stale pins.
/// properties with their <c>Fallout.*</c> equivalents, stripping stale pins, and adding or
/// removing the temporary <c>NuGet.Framework</c> pin for .NET SDK 10.0.400.
/// </summary>
/// <param name="original">The original <c>.csproj</c> file content.</param>
/// <param name="falloutVersion">The Fallout version to pin into rewritten inline-versioned references.</param>
/// <param name="isBuildProject"><c>true</c> when <paramref name="original"/> is <c>_build.csproj</c>.</param>
/// <returns>The rewritten content and the number of edits made.</returns>
private static RewriteResult Rewrite(string original, string falloutVersion)
private static RewriteResult Rewrite(string original, string falloutVersion, bool isBuildProject)
{
var edits = 0;
var content = original;
Expand Down Expand Up @@ -139,7 +157,8 @@ private static RewriteResult Rewrite(string original, string falloutVersion)
return string.Empty;
});

return HandleMsBuildVariable(falloutVersion, content, edits);
var result = HandleMsBuildVariable(falloutVersion, content, edits);
return HandleNugetFrameworkPin(result, falloutVersion, isBuildProject);
}

// Pass 5 — extract variables used by Fallout.* PackageReferences, decouple the ones ambiguously
Expand Down Expand Up @@ -267,4 +286,70 @@ private static (string content, int edits) BumpVariableProperties(string content

return (content, edits);
}

// Pass 6 — add the NuGet.Framework pin on `_build.csproj` only, and only when that
// project targets modern .NET (NuGet.Framework 7.9.0 dropped netstandard2.0 / .NET
// Framework). Strip the pin once FalloutVersion is on the marker major, or when the
// build project cannot take the package.
private static RewriteResult HandleNugetFrameworkPin(
RewriteResult result, string falloutVersion, bool isBuildProject)
{
if (!isBuildProject)
{
return result;
}

var content = result.Content;
var edits = result.EditCount;
var major = new Version(falloutVersion).Major;
var pin = nugetFrameworkPinPattern.Match(content);
var canPin = CanPinNugetFramework(content);

if (pin.Success)
{
if (major == int.Parse(pin.Groups["major"].Value) || !canPin)
{
return new RewriteResult(nugetFrameworkPinPattern.Replace(content, string.Empty, 1), edits + 1);
}

return result;
}

// This pin is a 10.x workaround. v11+ only strips a marker that already names that major.
if (major != 10 || !canPin)
{
return result;
}

var deleteAtMajor = major + 1;
var itemGroupClose = content.IndexOf("</ItemGroup>", StringComparison.Ordinal);
if (itemGroupClose < 0)
{
return result;
}

var newLine = content.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
var block = newLine
+ $" <!-- fallout-migrate:delete-at-v{deleteAtMajor}:start -->" + newLine
+ " <!-- Pin the NuGet.Framework version, so .NET 10.0.400 does not cause the build to fail. -->" + newLine
+ @" <PackageReference Include=""NuGet.Framework"" Version=""7.9.0"" />" + newLine
+ $" <!-- fallout-migrate:delete-at-v{deleteAtMajor}:end -->" + newLine;

return new RewriteResult(content.Insert(content.LastIndexOf('\n', itemGroupClose) + 1, block), edits + 1);
}

private static bool CanPinNugetFramework(string content)
{
var match = targetFrameworkElementPattern.Match(content);
if (!match.Success)
{
return false;
}

return match.Groups["value"].Value
.Split(';')
.Select(moniker => moniker.Trim())
.Where(moniker => moniker.Length > 0)
.All(TargetFrameworkMonikers.TargetsModernDotNet);
}
}
126 changes: 119 additions & 7 deletions tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ public async Task Recognizes_a_version_variable_prefixed_with_Nuke()
</Project>
""";
(tempDirectory / "build" / "_build.csproj").WriteAllText(input);

await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
Expand Down Expand Up @@ -341,7 +341,7 @@ public async Task Leaves_an_arbitrary_version_variable_alone_but_updates_the_ver
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input);

await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
Expand All @@ -367,7 +367,7 @@ public async Task Leaves_an_unreferenced_arbitrary_variable_alone()
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input);

await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
Expand All @@ -392,7 +392,7 @@ public async Task Does_not_bump_variables_used_for_non_Fallout_packages()
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false);

await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
Expand All @@ -417,7 +417,7 @@ public async Task Decouples_ambiguously_used_variables()
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input);

await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
Expand All @@ -441,7 +441,7 @@ public async Task Decouples_ambiguously_used_variables_even_when_no_property_gro
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input);

await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
Expand All @@ -468,7 +468,7 @@ public async Task Decouples_ambiguously_used_variables_when_a_property_group_but
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input);

await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();
Expand All @@ -479,4 +479,116 @@ public async Task Decouples_ambiguously_used_variables_when_a_property_group_but
.And.Contain("$(FalloutVersion)", Exactly.Once())
.And.Contain("$(PkgVersion)");
}

[Fact]
public async Task Correctly_adds_nuget_framework_package_version_pin()
{
const string input = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false);

context.FalloutVersion = "10.4.2";
await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();

buildCsproj.Should().Be("""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

<!-- fallout-migrate:delete-at-v11:start -->
<!-- Pin the NuGet.Framework version, so .NET 10.0.400 does not cause the build to fail. -->
<PackageReference Include="NuGet.Framework" Version="7.9.0" />
<!-- fallout-migrate:delete-at-v11:end -->
</ItemGroup>
</Project>
""");
}

[Fact]
public async Task Removes_nuget_framework_package_version_pin_on_v11()
{
const string input = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
</PropertyGroup>
<ItemGroup>

<!-- fallout-migrate:delete-at-v11:start -->
<!-- Pin the NuGet.Framework version, so .NET 10.0.400 does not cause the build to fail. -->
<PackageReference Include="NuGet.Framework" Version="7.9.0" />
<!-- fallout-migrate:delete-at-v11:end -->
</ItemGroup>
</Project>
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false);

context.FalloutVersion = "11.0.0";
await new RewriteCsprojsStep().ExecuteAsync(context, summary);

var buildCsproj = (tempDirectory / "build" / "_build.csproj").ReadAllText();

buildCsproj.Should().Be("""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>
""");
}

[Fact]
public async Task Does_not_add_nuget_framework_pin_to_a_non_build_csproj()
{
const string input = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>
""";

(tempDirectory / "Lib.csproj").WriteAllText(input, eofLineBreak: false);

context.FalloutVersion = "10.4.2";
await new RewriteCsprojsStep().ExecuteAsync(context, summary);

(tempDirectory / "Lib.csproj").ReadAllText().Should().Be(input);
}

[Fact]
public async Task Does_not_add_nuget_framework_pin_when_the_build_project_targets_netframework()
{
const string input = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>
""";

(tempDirectory / "build" / "_build.csproj").WriteAllText(input, eofLineBreak: false);

context.FalloutVersion = "10.4.2";
await new RewriteCsprojsStep().ExecuteAsync(context, summary);

(tempDirectory / "build" / "_build.csproj").ReadAllText().Should().Be(input);
}
}
Loading