Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 14 additions & 2 deletions TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
seenMethods, seenFullMethods, seenProperties, seenEvents, ref memberIdCounter);
}

// For class targets, the class itself provides the concrete static impl that
// satisfies any static-abstract interface members (#5677). The mock only needs
// to override instance-virtual surface — emitting a bridge interface would
// produce CS0527 (class in interface list) and CS0540 (explicit interface impl
// on a type that doesn't list the interface).
var collectStaticAbstractFromInterfaces = typeSymbol.TypeKind != TypeKind.Class;

foreach (var iface in interfaces)
{
string? explicitInterfaceName = null;
Expand All @@ -53,7 +60,7 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
{
if (member.IsStatic)
{
if (member.IsAbstract)
if (member.IsAbstract && collectStaticAbstractFromInterfaces)
{
CollectStaticAbstractMember(member, interfaceFqn, methods, properties, events, seenMethods, seenProperties, seenEvents, ref memberIdCounter);
}
Expand Down Expand Up @@ -191,6 +198,11 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
? new[] { typeSymbol }.Concat(typeSymbol.AllInterfaces)
: typeSymbol.AllInterfaces.AsEnumerable();

// For class targets, the class itself provides the concrete static impl that
// satisfies any static-abstract interface members (#5677). Skip emitting bridge
// dispatch members in that case.
var collectStaticAbstractFromInterfaces = typeSymbol.TypeKind != TypeKind.Class;

foreach (var iface in interfaces)
{
var interfaceFqn = iface.GetFullyQualifiedName();
Expand All @@ -199,7 +211,7 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
{
if (member.IsStatic)
{
if (member.IsAbstract)
if (member.IsAbstract && collectStaticAbstractFromInterfaces)
{
CollectStaticAbstractMember(member, interfaceFqn, methods, properties, events, seenMethods, seenProperties, seenEvents, ref memberIdCounter);
}
Expand Down
6 changes: 5 additions & 1 deletion TUnit.Mocks.SourceGenerator/MockGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ private static void GenerateSingleTypeMock(SourceProductionContext spc, MockType
{
var fileName = GetSafeFileName(model);

if (model.HasStaticAbstractMembers)
// Bridge interface only makes sense for interface targets — a class already
// provides the concrete static impl for any static-abstract interface members
// it implements (#5677). Emitting a bridge with a class in its base list is
// CS0527; emitting explicit interface impls without the interface is CS0540.
if (model.HasStaticAbstractMembers && model.IsInterface)
{
var bridgeSource = MockBridgeBuilder.Build(model);
spc.AddSource($"{fileName}_MockBridge.g.cs", bridgeSource);
Expand Down
46 changes: 42 additions & 4 deletions TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,22 @@ public interface ITagLarge : ITagSmall
// ("does not implement IHasIndexer.this[int]") because the mock-impl builder
// skips indexer emission without providing a stub. Tracked as a separate
// generator gap — not in scope of the #5673 fix.
// ─── T15 SKIPPED. Mocking a class that implements a static-abstract interface
// hits the bridge builder, which treats the target as an interface ("Type
// in interface list is not an interface"). Separate generator issue.

// ─── T15. Class implementing a static-abstract interface ────────────────────

#if NET8_0_OR_GREATER
public interface IStaticAbstractFactory
{
static abstract IStaticAbstractFactory Create();
int InstanceValue { get; }
}

public class StaticAbstractImpl : IStaticAbstractFactory
{
public static IStaticAbstractFactory Create() => new StaticAbstractImpl();
public virtual int InstanceValue => 99;
}
#endif

// ─── T16. IAsyncEnumerable with [EnumeratorCancellation] token ──────────────

Expand Down Expand Up @@ -433,7 +446,32 @@ public async Task T13_Derived_Interface_New_Property_Redeclaration()
await Assert.That(asSmall.Tag).IsEqualTo((short)0);
}

// T14, T15 tests elided — see the SKIPPED notes above the type declarations.
// T14 test elided — see the SKIPPED note above the type declarations.

// ── T15 (net8.0+ only — static abstract requires runtime support) ──

#if NET8_0_OR_GREATER
[Test]
public async Task T15_Class_Implementing_Static_Abstract_Interface_Mockable()
{
// Mocking a class whose interface has static-abstract members should still work:
// the class provides the concrete static impl; the mock only needs to override
// the instance-virtual surface. No bridge interface is required for class targets.
var mock = StaticAbstractImpl.Mock();
mock.InstanceValue.Returns(42);

await Assert.That(mock.Object.InstanceValue).IsEqualTo(42);
mock.InstanceValue.WasCalled(Times.Once);

// The class's concrete static impl is unaffected — direct call still works
// and returns a real instance, NOT routed through the mock engine. The static
// method's declared return type IStaticAbstractFactory itself can't be a type
// argument (CS8920), so observe the result as `object`.
object? created = StaticAbstractImpl.Create();
await Assert.That(created).IsNotNull();
await Assert.That(created).IsTypeOf<StaticAbstractImpl>();
}
#endif

// ── T16 ──

Expand Down
Loading