diff --git a/fallout.slnx b/fallout.slnx
index d954d9003..c994a30a5 100644
--- a/fallout.slnx
+++ b/fallout.slnx
@@ -51,6 +51,7 @@
+
diff --git a/src/Fallout.Common/ProjectModel/TransitionShims.cs b/src/Fallout.Common/ProjectModel/TransitionShims.cs
new file mode 100644
index 000000000..1de3d7348
--- /dev/null
+++ b/src/Fallout.Common/ProjectModel/TransitionShims.cs
@@ -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.
+
+///
+/// Transition shim for the relocated . See the file-level
+/// remarks — shallow by design; run fallout-migrate for a complete rewrite.
+///
+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;
+}
+
+///
+/// Transition shim for the relocated . Deserializes
+/// into the annotated member's declared type, so [Solution] readonly Solution Solution; against
+/// the shim above resolves correctly.
+///
+public class SolutionAttribute(string relativePath)
+ : global::Fallout.Solutions.SolutionAttribute(relativePath)
+{
+ public SolutionAttribute()
+ : this(relativePath: null)
+ {
+ }
+}
diff --git a/src/Fallout.Migrate/Steps/CodeRewriter.cs b/src/Fallout.Migrate/Steps/CodeRewriter.cs
index d90858e38..2549f771a 100644
--- a/src/Fallout.Migrate/Steps/CodeRewriter.cs
+++ b/src/Fallout.Migrate/Steps/CodeRewriter.cs
@@ -4,12 +4,22 @@
namespace Fallout.Migrate.Steps;
///
-/// Rewrites .cs files: Nuke.* namespace prefixes become Fallout., and the bare
-/// NukeBuild/INukeBuild types become FalloutBuild/IFalloutBuild.
-/// Driven by .
+/// Rewrites .cs files: Nuke.* namespace prefixes become Fallout., the bare
+/// NukeBuild/INukeBuild types become FalloutBuild/IFalloutBuild, and the
+/// solution-model namespace (which moved out of *.Common.ProjectModel in v11) becomes
+/// Fallout.Solutions. Driven by .
///
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
@@ -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, _ =>
{
edits++;
return "Fallout.";
diff --git a/src/Fallout.SourceGenerators/TransitionShimGenerator.cs b/src/Fallout.SourceGenerators/TransitionShimGenerator.cs
index a2eab8a93..0736dac58 100644
--- a/src/Fallout.SourceGenerators/TransitionShimGenerator.cs
+++ b/src/Fallout.SourceGenerators/TransitionShimGenerator.cs
@@ -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 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 ExceptPrefixes { get; }
}
private static ImmutableArray ExtractMarkers(ISymbol target)
@@ -114,7 +120,20 @@ private static ImmutableArray 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.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();
}
@@ -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;
}
@@ -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);
}
@@ -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)
@@ -684,6 +719,14 @@ public ShimAllPublicTypesUnderAttribute(string fromNamespacePrefix, string toNam
public string FromNamespacePrefix { get; }
public string ToNamespacePrefix { get; }
+
+ ///
+ /// Sub-namespaces under fromNamespacePrefix 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.
+ ///
+ public string[]? ExceptNamespacePrefixes { get; set; }
}
""";
}
diff --git a/src/Shims/Nuke.Common/ShimMarker.cs b/src/Shims/Nuke.Common/ShimMarker.cs
index 0f314f14e..9c643102c 100644
--- a/src/Shims/Nuke.Common/ShimMarker.cs
+++ b/src/Shims/Nuke.Common/ShimMarker.cs
@@ -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
diff --git a/tests/Consumers/Fallout.Consumer.ProjectModelShim/Build.cs b/tests/Consumers/Fallout.Consumer.ProjectModelShim/Build.cs
new file mode 100644
index 000000000..fa1f82c03
--- /dev/null
+++ b/tests/Consumers/Fallout.Consumer.ProjectModelShim/Build.cs
@@ -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(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 ?? "");
+ });
+}
diff --git a/tests/Consumers/Fallout.Consumer.ProjectModelShim/Fallout.Consumer.ProjectModelShim.csproj b/tests/Consumers/Fallout.Consumer.ProjectModelShim/Fallout.Consumer.ProjectModelShim.csproj
new file mode 100644
index 000000000..6080d9cdb
--- /dev/null
+++ b/tests/Consumers/Fallout.Consumer.ProjectModelShim/Fallout.Consumer.ProjectModelShim.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net10.0
+ Exe
+ false
+ Fallout.Consumer.ProjectModelShim
+ $(NoWarn);CS0649
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Fallout.Migrate.Specs/CodeRewriterSpecs.cs b/tests/Fallout.Migrate.Specs/CodeRewriterSpecs.cs
index 37bab7dcc..d156a60c9 100644
--- a/tests/Fallout.Migrate.Specs/CodeRewriterSpecs.cs
+++ b/tests/Fallout.Migrate.Specs/CodeRewriterSpecs.cs
@@ -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;");
+ }
}
diff --git a/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Enabled_code_generation#Solution.g.verified.cs b/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Enabled_code_generation#Solution.g.verified.cs
index 22e095752..01f7da782 100644
--- a/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Enabled_code_generation#Solution.g.verified.cs
+++ b/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Enabled_code_generation#Solution.g.verified.cs
@@ -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");
diff --git a/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Enabled_code_generation_with_fancy_naming#Solution.g.verified.cs b/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Enabled_code_generation_with_fancy_naming#Solution.g.verified.cs
index aec0fb68b..cf958a75b 100644
--- a/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Enabled_code_generation_with_fancy_naming#Solution.g.verified.cs
+++ b/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Enabled_code_generation_with_fancy_naming#Solution.g.verified.cs
@@ -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");
diff --git a/tests/Fallout.SourceGenerators.Specs/TransitionShimGeneratorSpecs.EmitsShimsForEachKindAndSkipsHardTier#ShimAllPublicTypesUnderAttribute.g.verified.cs b/tests/Fallout.SourceGenerators.Specs/TransitionShimGeneratorSpecs.EmitsShimsForEachKindAndSkipsHardTier#ShimAllPublicTypesUnderAttribute.g.verified.cs
index 274831710..f48c1f7fe 100644
--- a/tests/Fallout.SourceGenerators.Specs/TransitionShimGeneratorSpecs.EmitsShimsForEachKindAndSkipsHardTier#ShimAllPublicTypesUnderAttribute.g.verified.cs
+++ b/tests/Fallout.SourceGenerators.Specs/TransitionShimGeneratorSpecs.EmitsShimsForEachKindAndSkipsHardTier#ShimAllPublicTypesUnderAttribute.g.verified.cs
@@ -20,4 +20,12 @@ public ShimAllPublicTypesUnderAttribute(string fromNamespacePrefix, string toNam
public string FromNamespacePrefix { get; }
public string ToNamespacePrefix { get; }
+
+ ///
+ /// Sub-namespaces under fromNamespacePrefix 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.
+ ///
+ public string[]? ExceptNamespacePrefixes { get; set; }
}
\ No newline at end of file