Skip to content
Open
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
1 change: 1 addition & 0 deletions fallout.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<Project Path="tests\Benchmarks\Fallout.Persistence.Solution.Benchmarks\Fallout.Persistence.Solution.Benchmarks.csproj" />
<Project Path="tests\Consumers\Nuke.Consumer\Nuke.Consumer.csproj" />
<Project Path="tests\Consumers\Fallout.Consumer.Local\Fallout.Consumer.Local.csproj" />
<Project Path="tests\Consumers\Fallout.Consumer.ProjectModelShim\Fallout.Consumer.ProjectModelShim.csproj" />
<Project Path="tests\Consumers\Fallout.Consumer.NuGet\Fallout.Consumer.NuGet.csproj" />
<Project Path="tests\Fallout.Tooling.Specs\Fallout.Tooling.Specs.csproj" />
<Project Path="tests\Fallout.Utilities.Specs\Fallout.Utilities.Specs.csproj" />
Expand Down
54 changes: 54 additions & 0 deletions src/Fallout.Common/ProjectModel/TransitionShims.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Fallout.Common.IO;
using Fallout.Persistence.Solution.Model;

namespace Fallout.Common.ProjectModel;

// Transition shims for the solution entry-point types, which shipped under
// `Fallout.Common.ProjectModel` in Fallout 11.0.1–11.0.12 and moved to the
// dedicated `Fallout.Solutions` namespace in 11.0.13 (#248 / #257). Mirroring the
// pair below keeps the canonical consumer pattern
//
// using Fallout.Common.ProjectModel;
// [Solution] readonly Solution Solution;
//
// compiling for consumers upgrading from those releases, without their having to
// run `fallout-migrate` first.
//
// SHALLOW BY DESIGN. This is an entry-point grace period, not a full alias of the
// old namespace. Navigating the graph (e.g. `solution.Projects`) hands back
// canonical `Fallout.Solutions.*` instances, so intermediate variables typed
// against `Fallout.Common.ProjectModel` will not bind, and `GenerateProjects`
// source-generation keys off the canonical attribute type and won't fire through
// the shim. `fallout-migrate` (or the Nuke→Fallout codefix) is the complete
// migration path — it rewrites every reference, shallow and deep. This mirrors the
// same ceiling the generated `Nuke.Common.ProjectModel` transition shim has.

/// <summary>
/// Transition shim for the relocated <see cref="Fallout.Solutions.Solution"/>. See the file-level
/// remarks — shallow by design; run <c>fallout-migrate</c> for a complete rewrite.
/// </summary>
public class Solution(SolutionModel model, AbsolutePath path = null)
: global::Fallout.Solutions.Solution(model, path)
{
// C# does not inherit user-defined conversion operators onto a subclass, so the
// canonical Solution's string / AbsolutePath coercions would silently disappear
// on the shim. Re-expose them so `string s = Solution;` and `AbsolutePath p = Solution;`
// keep working. The parameter type (shim Solution) is more specific than the
// canonical's, so these win over the inherited operators without ambiguity.
public static implicit operator string(Solution solution) => (global::Fallout.Solutions.Solution)solution;
public static implicit operator AbsolutePath(Solution solution) => (global::Fallout.Solutions.Solution)solution;
}

/// <summary>
/// Transition shim for the relocated <see cref="Fallout.Solutions.SolutionAttribute"/>. Deserializes
/// into the annotated member's declared type, so <c>[Solution] readonly Solution Solution;</c> against
/// the shim <see cref="Solution"/> above resolves correctly.
/// </summary>
public class SolutionAttribute(string relativePath)
: global::Fallout.Solutions.SolutionAttribute(relativePath)
{
public SolutionAttribute()
: this(relativePath: null)
{
}
}
24 changes: 20 additions & 4 deletions src/Fallout.Migrate/Steps/CodeRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@
namespace Fallout.Migrate.Steps;

/// <summary>
/// Rewrites <c>.cs</c> files: <c>Nuke.*</c> namespace prefixes become <c>Fallout.</c>, and the bare
/// <c>NukeBuild</c>/<c>INukeBuild</c> types become <c>FalloutBuild</c>/<c>IFalloutBuild</c>.
/// Driven by <see cref="RewriteCsFilesStep"/>.
/// Rewrites <c>.cs</c> files: <c>Nuke.*</c> namespace prefixes become <c>Fallout.</c>, the bare
/// <c>NukeBuild</c>/<c>INukeBuild</c> types become <c>FalloutBuild</c>/<c>IFalloutBuild</c>, and the
/// solution-model namespace (which moved out of <c>*.Common.ProjectModel</c> in v11) becomes
/// <c>Fallout.Solutions</c>. Driven by <see cref="RewriteCsFilesStep"/>.
/// </summary>
internal static class CodeRewriter
{
// The solution types moved from `(Nuke|Fallout).Common.ProjectModel` to the
// dedicated `Fallout.Solutions` namespace in v11 (#248 + onion layering).
// Run this BEFORE the generic prefix swap so a NUKE-era reference lands on
// the canonical v11 namespace in one edit instead of the now-dead
// `Fallout.Common.ProjectModel`. Matching both source prefixes also fixes
// already-partially-migrated code. Mirrors the codefix mapping from #253.
private static readonly Regex projectModelNamespace =
new(@"\b(?:Nuke|Fallout)\.Common\.ProjectModel\b", RegexOptions.Compiled);

// Anchored prefix swap: `\bNuke\.` → `Fallout.`. Covers using directives,
// attribute references, qualified type names, namespace declarations.
// The trailing `(?=[A-Z])` lookahead avoids matching `Nuke.json` filenames
Expand All @@ -31,7 +41,13 @@ public static RewriteResult Rewrite(string original)
{
var edits = 0;

var content = namespacePrefix.Replace(original, _ =>
var content = projectModelNamespace.Replace(original, _ =>
{
edits++;
return "Fallout.Solutions";
});

content = namespacePrefix.Replace(content, _ =>
Comment on lines 14 to +50

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the new migration design, that new migrations should go into their dedicated `XxxStep".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, claude doesn't quite understand it yet. so this needs some work plus some agent.md adjustments. just down with a pretty bad cold this week so doing things slower and one at a time 😊

{
edits++;
return "Fallout.";
Expand Down
51 changes: 47 additions & 4 deletions src/Fallout.SourceGenerators/TransitionShimGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

private readonly struct ShimMarker
{
public ShimMarker(string fromPrefix, string toPrefix)
public ShimMarker(string fromPrefix, string toPrefix, ImmutableArray<string> exceptPrefixes)
{
FromPrefix = fromPrefix;
ToPrefix = toPrefix;
ExceptPrefixes = exceptPrefixes;
}
public string FromPrefix { get; }
public string ToPrefix { get; }

// Sub-namespaces under FromPrefix to leave unshimmed — e.g. a namespace
// whose types relocated elsewhere and are shimmed by a different marker.
// Without this, two markers can emit the same target shim type.
public ImmutableArray<string> ExceptPrefixes { get; }
}

private static ImmutableArray<ShimMarker> ExtractMarkers(ISymbol target)
Expand All @@ -114,7 +120,20 @@ private static ImmutableArray<ShimMarker> ExtractMarkers(ISymbol target)
var to = attr.ConstructorArguments[1].Value as string;
if (string.IsNullOrEmpty(from) || string.IsNullOrEmpty(to))
continue;
results.Add(new ShimMarker(from!, to!));

var except = ImmutableArray<string>.Empty;
foreach (var named in attr.NamedArguments)
{
if (named.Key != "ExceptNamespacePrefixes" || named.Value.IsNull)
continue;
except = named.Value.Values
.Select(v => v.Value as string)
.Where(s => !string.IsNullOrEmpty(s))
.Select(s => s!)
.ToImmutableArray();
}

results.Add(new ShimMarker(from!, to!, except));
}
return results.ToImmutable();
}
Expand Down Expand Up @@ -192,7 +211,7 @@ private static void CountFqnsInNamespace(INamespaceSymbol ns, ShimMarker marker,
if (string.IsNullOrEmpty(fullNamespace)) continue;
var matches = fullNamespace == marker.FromPrefix
|| fullNamespace.StartsWith(marker.FromPrefix + ".", StringComparison.Ordinal);
if (!matches) continue;
if (!matches || IsUnderExcludedPrefix(fullNamespace, marker)) continue;
var fqn = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
counts[fqn] = counts.TryGetValue(fqn, out var existing) ? existing + 1 : 1;
}
Expand All @@ -213,7 +232,7 @@ private static void VisitNamespace(SourceProductionContext ctx, INamespaceSymbol
if (string.IsNullOrEmpty(fullNamespace)) continue;
var matches = fullNamespace == marker.FromPrefix
|| fullNamespace.StartsWith(marker.FromPrefix + ".", StringComparison.Ordinal);
if (!matches) continue;
if (!matches || IsUnderExcludedPrefix(fullNamespace, marker)) continue;

EmitOrSkipType(ctx, type, marker, emittedHints, ambiguousFqns, handBridgedFqns);
}
Expand Down Expand Up @@ -532,6 +551,22 @@ private static string SwapNamespace(string original, ShimMarker marker)
return marker.ToPrefix + original.Substring(marker.FromPrefix.Length);
}

// True when the namespace sits under one of the marker's excluded prefixes
// (the prefix itself or a sub-namespace of it). Such types are left unshimmed
// by this marker — typically because another marker owns their shim.
private static bool IsUnderExcludedPrefix(string fullNamespace, ShimMarker marker)
{
if (marker.ExceptPrefixes.IsDefaultOrEmpty)
return false;
foreach (var except in marker.ExceptPrefixes)
{
if (fullNamespace == except
|| fullNamespace.StartsWith(except + ".", StringComparison.Ordinal))
return true;
}
return false;
}

private static string FormatGenericParameters(INamedTypeSymbol type)
{
if (type.TypeParameters.IsDefaultOrEmpty || type.TypeParameters.Length == 0)
Expand Down Expand Up @@ -684,6 +719,14 @@ public ShimAllPublicTypesUnderAttribute(string fromNamespacePrefix, string toNam

public string FromNamespacePrefix { get; }
public string ToNamespacePrefix { get; }

/// <summary>
/// Sub-namespaces under <c>fromNamespacePrefix</c> to leave unshimmed,
/// e.g. one whose types relocated to another namespace that a separate
/// marker already shims. Prevents two markers emitting the same target
/// shim type.
/// </summary>
public string[]? ExceptNamespacePrefixes { get; set; }
}
""";
}
9 changes: 8 additions & 1 deletion src/Shims/Nuke.Common/ShimMarker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@
// the Fallout.Common namespace despite being declared in the Fallout.Build
// project).

// ProjectModel is excluded here: those solution types relocated to
// Fallout.Solutions in v11 and are shimmed by the marker below. Fallout.Common
// also re-exports Solution/SolutionAttribute under Fallout.Common.ProjectModel as
// a Fallout-side entry-point grace shim (#257 mitigation) — without this
// exclusion, Rule 1 and the Fallout.Solutions rule would both emit
// Nuke.Common.ProjectModel.Solution, colliding (CS0263/CS0111).
[assembly: Fallout.Migrate.Shims.ShimAllPublicTypesUnder(
fromNamespacePrefix: "Fallout.Common",
toNamespacePrefix: "Nuke.Common")]
toNamespacePrefix: "Nuke.Common",
ExceptNamespacePrefixes = new[] { "Fallout.Common.ProjectModel" })]

// The solution-handling types moved from Fallout.Common.ProjectModel to the
// dedicated Fallout.Solutions namespace in v11 (see #248 and the broader
Expand Down
24 changes: 24 additions & 0 deletions tests/Consumers/Fallout.Consumer.ProjectModelShim/Build.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Pre-rename Fallout consumer pattern (Fallout 11.0.1–11.0.12), compiled against
// the Fallout.Common.ProjectModel transition shim. If a Build.cs from those
// releases stops compiling against the latest Fallout, this fails — protecting
// users upgrading across the SolutionModel → Solution / Fallout.Solutions rename.

using Fallout.Common;
using Fallout.Common.IO;
using Fallout.Common.ProjectModel; // the interim namespace, now served by the transition shim

internal class Build : FalloutBuild
{
public static int Main() => Execute<Build>(x => x.Default);

[Solution]
private readonly Solution Solution;

private Target Default => _ => _
.Executes(() =>
{
Serilog.Log.Information("hello from fallout consumer (Fallout.Common.ProjectModel shim)");
Serilog.Log.Information("solution name: {Name}", Solution?.Name ?? "<unbound>");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<RootNamespace>Fallout.Consumer.ProjectModelShim</RootNamespace>
<NoWarn>$(NoWarn);CS0649</NoWarn>
</PropertyGroup>

<!--
Compile-time sentinel for the Fallout.Common.ProjectModel transition shim.

The solution types shipped under `Fallout.Common.ProjectModel` in Fallout
11.0.1–11.0.12 and moved to `Fallout.Solutions` in 11.0.13 (#248 / #257).
Build.cs deliberately keeps the OLD `using Fallout.Common.ProjectModel;` +
`[Solution] readonly Solution Solution;` shape, so that if the shim in
src/Fallout.Common/ProjectModel/TransitionShims.cs regresses, this stops
compiling and CI fails — mirroring how Nuke.Consumer guards the
Nuke.Common.ProjectModel shim.
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Fallout.Common\Fallout.Common.csproj" />
<ProjectReference Include="..\..\..\src\Fallout.Build\Fallout.Build.csproj" />
<ProjectReference Include="..\..\..\src\Persistence\Fallout.Solution\Fallout.Solution.csproj" />
<ProjectReference Include="..\..\..\src\Fallout.Components\Fallout.Components.csproj" />
</ItemGroup>

</Project>
41 changes: 41 additions & 0 deletions tests/Fallout.Migrate.Specs/CodeRewriterSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,45 @@ public void DoesNotMatchLowercaseNukePrefix()
var result = CodeRewriter.Rewrite(input);
result.EditCount.Should().Be(0);
}

[Fact]
public void RewritesNukeProjectModelUsingToSolutions()
{
// The solution types moved to Fallout.Solutions in v11 — a NUKE-era
// `using` must land there, not on the dead Fallout.Common.ProjectModel.
const string input = "using Nuke.Common.ProjectModel;";
var result = CodeRewriter.Rewrite(input);
result.EditCount.Should().Be(1);
result.Content.Should().Be("using Fallout.Solutions;");
}

[Fact]
public void RewritesQualifiedNukeProjectModelTypeToSolutions()
{
const string input = "Nuke.Common.ProjectModel.Solution x;";
var result = CodeRewriter.Rewrite(input);
result.EditCount.Should().Be(1);
result.Content.Should().Be("Fallout.Solutions.Solution x;");
}

[Fact]
public void RewritesAlreadyPartiallyMigratedProjectModelNamespace()
{
// Code previously run through a prefix-only migrator lands on the dead
// Fallout.Common.ProjectModel; the ProjectModel rule salvages it.
const string input = "using Fallout.Common.ProjectModel;";
var result = CodeRewriter.Rewrite(input);
result.EditCount.Should().Be(1);
result.Content.Should().Be("using Fallout.Solutions;");
}

[Fact]
public void DoesNotMatchProjectModelAsPartOfAnotherIdentifier()
{
// `ProjectModelFoo` must not be truncated by the ProjectModel rule.
const string input = "using Nuke.Common.ProjectModelFoo;";
var result = CodeRewriter.Rewrite(input);
result.EditCount.Should().Be(1);
result.Content.Should().Be("using Fallout.Common.ProjectModelFoo;");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal class Solution(SolutionModel model, AbsolutePath path) : Fallout.Soluti
public Fallout.Solutions.Project Fallout_Components_Specs => this.GetProject("Fallout.Components.Specs");
public Fallout.Solutions.Project Fallout_Consumer_Local => this.GetProject("Fallout.Consumer.Local");
public Fallout.Solutions.Project Fallout_Consumer_NuGet => this.GetProject("Fallout.Consumer.NuGet");
public Fallout.Solutions.Project Fallout_Consumer_ProjectModelShim => this.GetProject("Fallout.Consumer.ProjectModelShim");
public Fallout.Solutions.Project Fallout_Core => this.GetProject("Fallout.Core");
public Fallout.Solutions.Project Fallout_Core_Specs => this.GetProject("Fallout.Core.Specs");
public Fallout.Solutions.Project Fallout_Migrate => this.GetProject("Fallout.Migrate");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal class Solution(SolutionModel model, AbsolutePath path) : Fallout.Soluti
public Fallout.Solutions.Project FalloutꞏComponentsꞏSpecs => this.GetProject("Fallout.Components.Specs");
public Fallout.Solutions.Project FalloutꞏConsumerꞏLocal => this.GetProject("Fallout.Consumer.Local");
public Fallout.Solutions.Project FalloutꞏConsumerꞏNuGet => this.GetProject("Fallout.Consumer.NuGet");
public Fallout.Solutions.Project FalloutꞏConsumerꞏProjectModelShim => this.GetProject("Fallout.Consumer.ProjectModelShim");
public Fallout.Solutions.Project FalloutꞏCore => this.GetProject("Fallout.Core");
public Fallout.Solutions.Project FalloutꞏCoreꞏSpecs => this.GetProject("Fallout.Core.Specs");
public Fallout.Solutions.Project FalloutꞏMigrate => this.GetProject("Fallout.Migrate");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ public ShimAllPublicTypesUnderAttribute(string fromNamespacePrefix, string toNam

public string FromNamespacePrefix { get; }
public string ToNamespacePrefix { get; }

/// <summary>
/// Sub-namespaces under <c>fromNamespacePrefix</c> to leave unshimmed,
/// e.g. one whose types relocated to another namespace that a separate
/// marker already shims. Prevents two markers emitting the same target
/// shim type.
/// </summary>
public string[]? ExceptNamespacePrefixes { get; set; }
}
Loading