From cb4fb9d4cd019823b28e2680304fbad87805c08c Mon Sep 17 00:00:00 2001 From: Lukas Gasselsberger Date: Sat, 11 Jul 2026 20:52:47 +0200 Subject: [PATCH] Recognizes version variables and updates the version accordingly --- .../Steps/RewriteCsprojsStep.cs | 36 +++++++++- .../MigrationIntegrationSpecs.cs | 38 +++++----- .../RewriteCsprojsStepSpecs.cs | 70 +++++++++++++++++++ 3 files changed, 125 insertions(+), 19 deletions(-) diff --git a/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs b/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs index b462a45cf..66d99ac70 100644 --- a/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs +++ b/src/Fallout.Migrate/Steps/RewriteCsprojsStep.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Fallout.Migrate.Common; @@ -19,7 +20,7 @@ internal sealed class RewriteCsprojsStep : IMigrationStep // a broken post-migrate build (#217). Tolerates extra attributes between Include and Version // (e.g. `PrivateAssets="all"`). private static readonly Regex nukePackageWithInlineVersionPattern = new( - @"(?[A-Z][A-Za-z0-9.]+)(?""[^>]*?\s+Version="")[^""]+", + @"(?[A-Z][A-Za-z0-9.]+)(?""[^>]*?\s+Version="")(?!\$\()[^""]+", RegexOptions.Compiled); // PackageReference / ProjectReference `Include="Nuke.X"` → `Include="Fallout.X"` — namespace @@ -28,13 +29,18 @@ internal sealed class RewriteCsprojsStep : IMigrationStep private static readonly Regex packageReferencePattern = new(@"(?<=\b(?:Include|Update|Remove)="")Nuke\.(?=[A-Z])", RegexOptions.Compiled); + // Detects MSBuild variables used in PackageReference Version attributes: Version="$(MyVar)" + private static readonly Regex packageReferenceVariablePattern = new( + @"]*?Version=""\$\((?[^)]+)\)""", + RegexOptions.Compiled); + // MSBuild element/property names that begin with `Nuke` followed by an uppercase // letter (e.g. ...). Limited to known consumer-facing names from // P3.5b so we don't rewrite unrelated user-defined identifiers that happen to start // with the literal "Nuke". private static readonly Regex msBuildPropertyPattern = new( @"\bNuke(?=" + - "(?:RootDirectory|ScriptDirectory|TelemetryVersion|BaseDirectory|BaseNamespace|" + + "(?:Version|RootDirectory|ScriptDirectory|TelemetryVersion|BaseDirectory|BaseNamespace|" + "UseNestedNamespaces|RepositoryUrl|UpdateReferences|ContinueOnError|TaskTimeout|" + "Timeout|TasksEnabled|DefaultExcludes|ExcludeBoot|ExcludeConfig|ExcludeLogs|" + "ExcludeDirectoryBuild|ExcludeCi|SpecificationFiles|ExternalFiles|TasksAssembly|" + @@ -109,6 +115,32 @@ public static RewriteResult Rewrite(string original, string falloutVersion) return string.Empty; }); + // Pass 4 — extract variables used in PackageReferences and bump their properties. + var variablesToBump = packageReferenceVariablePattern.Matches(content) + .Select(m => m.Groups["variable"].Value) + .ToHashSet(); + + // Always include FalloutVersion as they are canonical. + variablesToBump.Add("FalloutVersion"); + + foreach (var variable in variablesToBump) + { + var pattern = $"(?<=<{variable}>)[^<]+(?=)"; + content = Regex.Replace(content, + pattern, + m => + { + if (m.Value == falloutVersion) + { + return m.Value; + } + + edits++; + return falloutVersion; + + }); + } + return new RewriteResult(content, edits); } } diff --git a/tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs b/tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs index 53a937e8e..944f967cc 100644 --- a/tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs +++ b/tests/Fallout.Migrate.Specs/MigrationIntegrationSpecs.cs @@ -20,19 +20,21 @@ public async Task MigratesVanillaConsumerRepo() var summary = await migration.RunAsync(); // Build file rewritten end to end. - var buildCsproj = File.ReadAllText(Path.Combine(temp, "build", "_build.csproj")); + var buildCsproj = await File.ReadAllTextAsync(Path.Combine(temp, "build", "_build.csproj")); buildCsproj.Should().Contain(@"Include=""Fallout.Common"""); buildCsproj.Should().Contain(""); buildCsproj.Should().NotContain("Nuke.Common"); buildCsproj.Should().NotContain(""); + buildCsproj.Should().NotContain("NukeVersion"); + buildCsproj.Should().Contain("FalloutVersion", Exactly.Thrice()); - var buildCs = File.ReadAllText(Path.Combine(temp, "build", "Build.cs")); + var buildCs = await File.ReadAllTextAsync(Path.Combine(temp, "build", "Build.cs")); buildCs.Should().Contain("using Fallout.Common"); buildCs.Should().Contain(": FalloutBuild"); buildCs.Should().NotContain("using Nuke."); buildCs.Should().NotContain("NukeBuild"); - var buildSh = File.ReadAllText(Path.Combine(temp, "build.sh")); + var buildSh = await File.ReadAllTextAsync(Path.Combine(temp, "build.sh")); buildSh.Should().Contain("dotnet fallout"); buildSh.Should().Contain("FALLOUT_TELEMETRY_OPTOUT"); buildSh.Should().Contain(".fallout/temp"); @@ -60,12 +62,12 @@ public async Task DryRunDoesNotWriteFiles() try { - var beforeCsproj = File.ReadAllText(Path.Combine(temp, "build", "_build.csproj")); + var beforeCsproj = await File.ReadAllTextAsync(Path.Combine(temp, "build", "_build.csproj")); var beforeNukeDir = Directory.Exists(Path.Combine(temp, ".nuke")); var summary = await new Migration(temp, dryRun: true, TextWriter.Null).RunAsync(); - File.ReadAllText(Path.Combine(temp, "build", "_build.csproj")).Should().Be(beforeCsproj); + (await File.ReadAllTextAsync(Path.Combine(temp, "build", "_build.csproj"))).Should().Be(beforeCsproj); Directory.Exists(Path.Combine(temp, ".nuke")).Should().Be(beforeNukeDir); summary.FilesChanged.Should().BeGreaterThan(0); // counts intended edits } @@ -100,7 +102,7 @@ public async Task WarnsWhenBuildProjectTargetsOlderThanNet10() { var temp = CreateVanillaFixture(); var buildCsprojPath = Path.Combine(temp, "build", "_build.csproj"); - File.WriteAllText(buildCsprojPath, File.ReadAllText(buildCsprojPath).Replace("net10.0", "net8.0")); + await File.WriteAllTextAsync(buildCsprojPath, (await File.ReadAllTextAsync(buildCsprojPath)).Replace("net10.0", "net8.0")); try { @@ -120,23 +122,23 @@ public async Task BumpsDotNetVersionAndSdk() { var temp = CreateVanillaFixture(); var buildCsprojPath = Path.Combine(temp, "build", "_build.csproj"); - File.WriteAllText(buildCsprojPath, File.ReadAllText(buildCsprojPath).Replace("net10.0", "net8.0")); - File.WriteAllText(Path.Combine(temp, "global.json"), """ - { - "sdk": { - "version": "8.0.100", - "rollForward": "latestMinor" - } - } - """); + await File.WriteAllTextAsync(buildCsprojPath, (await File.ReadAllTextAsync(buildCsprojPath)).Replace("net10.0", "net8.0")); + await File.WriteAllTextAsync(Path.Combine(temp, "global.json"), """ + { + "sdk": { + "version": "8.0.100", + "rollForward": "latestMinor" + } + } + """); try { await new Migration(temp, dryRun: false, TextWriter.Null).RunAsync(); - File.ReadAllText(buildCsprojPath).Should().Contain("net10.0"); + (await File.ReadAllTextAsync(buildCsprojPath)).Should().Contain("net10.0"); - var globalJson = File.ReadAllText(Path.Combine(temp, "global.json")); + var globalJson = await File.ReadAllTextAsync(Path.Combine(temp, "global.json")); globalJson.Should().Contain(@"""version"": ""10.0.100"""); } finally @@ -159,9 +161,11 @@ private static string CreateVanillaFixture() net10.0 .\.. 1 + 9.0.0 + """); diff --git a/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs index e11732f9f..e1cce4f40 100644 --- a/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs +++ b/tests/Fallout.Migrate.Specs/RewriteCsprojsStepSpecs.cs @@ -183,4 +183,74 @@ public void LeavesOtherSystemPackagesAlone() result.EditCount.Should().Be(0); result.Content.Should().Be(input); } + + [Fact] + public void Recognizes_a_version_variable_prefixed_with_Nuke() + { + const string input = """ + + + 10.1.0 + + + + + + + + + """; + + var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); + + result.Content.Should().NotContain("NukeVersion") + .And.Contain("$(FalloutVersion)", Exactly.Twice()) + .And.Contain("11.0.0"); + } + + [Fact] + public void Leaves_an_arbitrary_version_variable_alone_but_updates_the_version() + { + const string input = """ + + + 10.3.49 + + + + + + + + + """; + + var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); + + result.Content.Should().NotContain("FalloutVersion") + .And.Contain("$(CiProjectVersion)", Exactly.Twice()) + .And.Contain("11.0.0"); + } + + [Fact] + public void Leaves_an_unreferenced_arbitrary_variable_alone() + { + const string input = """ + + + 10.3.49 + 10.3.49 + + + + + + """; + + var result = RewriteCsprojsStep.Rewrite(input, TestFalloutVersion); + + result.Content.Should().Contain("10.3.49") + .And.Contain("11.0.0") + .And.Contain("$(CiProjectVersion)", Exactly.Once()); + } }