Skip to content
Merged
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
46 changes: 41 additions & 5 deletions src/Fallout.SourceGenerators/TransitionShimGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ private static void EmitShimsForMarker(SourceProductionContext ctx, Compilation
fqnCounts.Where(kv => kv.Value > 1).Select(kv => kv.Key),
StringComparer.Ordinal);

// Pre-pass: collect FQNs of every top-level public type the consuming
// compilation already declares under the target shim prefix. A hand-
// written bridge at the target FQN is the consumer's authoritative
// answer — the generator skips that canonical type silently (no
// emission, no SHIM001).
var handBridged = new HashSet<string>(StringComparer.Ordinal);
CollectHandBridgedFqns(compilation.SourceModule.GlobalNamespace, marker.ToPrefix, handBridged);

// The same logical type can be reached via multiple referenced assemblies.
// Dedupe hint names so AddSource doesn't throw.
var emittedHints = new HashSet<string>(StringComparer.Ordinal);
Expand All @@ -140,8 +148,28 @@ private static void EmitShimsForMarker(SourceProductionContext ctx, Compilation
if (!assemblyRef.Name.StartsWith("Fallout.", StringComparison.Ordinal))
continue;

VisitNamespace(ctx, assemblyRef.GlobalNamespace, marker, emittedHints, ambiguous);
VisitNamespace(ctx, assemblyRef.GlobalNamespace, marker, emittedHints, ambiguous, handBridged);
}
}

private static void CollectHandBridgedFqns(INamespaceSymbol ns, string toPrefix, HashSet<string> sink)
{
var fullNs = ns.ToDisplayString();
var inScope = !string.IsNullOrEmpty(fullNs)
&& (fullNs == toPrefix || fullNs.StartsWith(toPrefix + ".", StringComparison.Ordinal));
if (inScope)
{
foreach (var type in ns.GetTypeMembers())
{
if (type.DeclaredAccessibility != Accessibility.Public) continue;
// Compose the metadata-style FQN: namespace + "." + MetadataName.
// MetadataName encodes arity (e.g. `Foo`1` for `Foo<T>`), so this
// matches arity correctly when we look it up later.
sink.Add(fullNs + "." + type.MetadataName);
}
}
foreach (var child in ns.GetNamespaceMembers())
CollectHandBridgedFqns(child, toPrefix, sink);
}

private static void CountFqnsInNamespace(INamespaceSymbol ns, ShimMarker marker, Dictionary<string, int> counts)
Expand All @@ -161,7 +189,7 @@ private static void CountFqnsInNamespace(INamespaceSymbol ns, ShimMarker marker,
CountFqnsInNamespace(child, marker, counts);
}

private static void VisitNamespace(SourceProductionContext ctx, INamespaceSymbol ns, ShimMarker marker, HashSet<string> emittedHints, HashSet<string> ambiguousFqns)
private static void VisitNamespace(SourceProductionContext ctx, INamespaceSymbol ns, ShimMarker marker, HashSet<string> emittedHints, HashSet<string> ambiguousFqns, HashSet<string> handBridgedFqns)
{
foreach (var type in ns.GetTypeMembers())
{
Expand All @@ -176,19 +204,27 @@ private static void VisitNamespace(SourceProductionContext ctx, INamespaceSymbol
|| fullNamespace.StartsWith(marker.FromPrefix + ".", StringComparison.Ordinal);
if (!matches) continue;

EmitOrSkipType(ctx, type, marker, emittedHints, ambiguousFqns);
EmitOrSkipType(ctx, type, marker, emittedHints, ambiguousFqns, handBridgedFqns);
}
foreach (var child in ns.GetNamespaceMembers())
VisitNamespace(ctx, child, marker, emittedHints, ambiguousFqns);
VisitNamespace(ctx, child, marker, emittedHints, ambiguousFqns, handBridgedFqns);
}

private static void EmitOrSkipType(SourceProductionContext ctx, INamedTypeSymbol type, ShimMarker marker, HashSet<string> emittedHints, HashSet<string> ambiguousFqns)
private static void EmitOrSkipType(SourceProductionContext ctx, INamedTypeSymbol type, ShimMarker marker, HashSet<string> emittedHints, HashSet<string> ambiguousFqns, HashSet<string> handBridgedFqns)
{
// Skip nested types at top level — they get emitted inside their
// declaring shim's source.
if (type.ContainingType is not null)
return;

// If the consumer hand-wrote a shim at the target FQN, treat that as the
// authoritative bridge. Skip both emission and the SHIM001 diagnostic —
// the canonical type is covered, just not by us.
var targetNs = SwapNamespace(type.ContainingNamespace.ToDisplayString(), marker);
var targetMetadataFqn = targetNs + "." + type.MetadataName;
if (handBridgedFqns.Contains(targetMetadataFqn))
return;

var fqn = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
if (ambiguousFqns.Contains(fqn))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,52 @@ public static void VoidReturn(string s) { }
return Verifier.Verify(result);
}

// Hand-bridge suppression: when the consuming compilation declares a type at
// the target shim FQN, the generator treats that as the authoritative bridge —
// no emission, no SHIM001 (even for canonical kinds that would otherwise be
// skipped as Hard tier). Mirrors the session-4 CI host pattern in Nuke.Common.
[Fact]
public void SkipsCanonicalTypesAlreadyHandBridgedByConsumer()
{
var canonical = CompileCanonicalAssembly("""
namespace Fallout.Common
{
public sealed class HandBridgedSealed { public HandBridgedSealed() {} }
public class HandBridgedRegular { public HandBridgedRegular() {} }
}
""");

var shimCompilation = CSharpCompilation.Create("Nuke.HandBridged",
new[]
{
CSharpSyntaxTree.ParseText("""
[assembly: Fallout.Migrate.Shims.ShimAllPublicTypesUnder("Fallout.Common", "Nuke.Common")]
"""),
CSharpSyntaxTree.ParseText("""
namespace Nuke.Common
{
public static class HandBridgedSealed { }
public static class HandBridgedRegular { }
}
"""),
},
Basic.Reference.Assemblies.NetStandard20.References.All
.Concat(new[] { canonical }),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var driver = CSharpGeneratorDriver.Create(new TransitionShimGenerator());
var result = driver.RunGenerators(shimCompilation).GetRunResult();

// No source emitted for the hand-bridged canonical types.
result.GeneratedTrees
.Where(t => !t.FilePath.EndsWith("ShimAllPublicTypesUnderAttribute.g.cs", System.StringComparison.Ordinal))
.Should().BeEmpty();

// And no SHIM001 diagnostic for either hand-bridged type — the sealed
// one would normally warn, the regular one would normally emit.
result.Diagnostics.Should().BeEmpty();
}

[Fact]
public void EmitsNothingWhenNoMarkerAttributePresent()
{
Expand Down
Loading