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
Original file line number Diff line number Diff line change
Expand Up @@ -2332,6 +2332,10 @@ private static void AppendMockSubject_ImplementClass_AddMethod(StringBuilder sb,
string wpc = Helpers.GetUniqueLocalVariableName("wpc", method.Parameters);
string wraps = Helpers.GetUniqueLocalVariableName("wraps", method.Parameters);
bool supportsWrapping = !explicitInterfaceImplementation && method is { IsStatic: false, IsProtected: false, };
// A method that hides a base member (`new`) is emitted as an explicit interface implementation.
// The wrapped instance must be cast to the declaring interface, otherwise the delegated call
// binds to the hiding member instead: wrong return type (CS0266) or missing constraints (CS0452/CS0453).
string wrapsType = method.ExplicitImplementation ?? className;
bool isAbstractOrInterface = isClassInterface || method.IsAbstract;

StringBuilder sb2 = new();
Expand Down Expand Up @@ -2452,7 +2456,7 @@ private static void AppendMockSubject_ImplementClass_AddMethod(StringBuilder sb,

if (supportsWrapping)
{
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(className)
sb.Append("\t\t\t\tif (").Append(mockRegistry).Append(".Wraps is ").Append(wrapsType)
.Append(' ').Append(wraps).Append(')').AppendLine();
sb.Append("\t\t\t\t{").AppendLine();
if (method.ReturnType != Type.Void)
Expand Down
168 changes: 168 additions & 0 deletions Tests/Mockolate.SourceGenerators.Tests/MockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,174 @@ await That(result.Sources["Mock.MyClassWithSealedProperties.g.cs"])
.DoesNotContain("override int MyProperty");
}

[Fact]
public async Task HiddenGenericMethod_ShouldDelegateWrappingToDeclaringInterface()
{
GeneratorResult result = Generator
.Run("""
using System.Collections.Generic;
using Mockolate;

namespace MyCode;

public class Program
{
public static void Main(string[] args)
{
_ = ITest.CreateMock();
}
}

public interface ITestParent
{
IEnumerable<T> Get<T>() where T : notnull;
}

public interface ITest : ITestParent
{
new IList<T> Get<T>() where T : notnull;
}
""");

await That(result.Diagnostics).IsEmpty();
await That(result.Sources["Mock.ITest.g.cs"])
.Contains("""
if (this.MockRegistry.Wraps is global::MyCode.ITestParent wraps)
{
wrappedResult = wraps.Get<T>();
""").IgnoringNewlineStyle()
.Because(
"the explicit implementation of the hidden member must delegate to the declaring interface, not to the hiding member");
}

[Fact]
public async Task HiddenGenericMethod_WithAdditionalConstraints_ShouldNotDelegateToHidingMember()
{
GeneratorResult result = Generator
.Run("""
using System.Collections.Generic;
using Mockolate;

namespace MyCode;

public class Program
{
public static void Main(string[] args)
{
_ = ITest.CreateMock();
}
}

public interface ITestParent
{
IEnumerable<T> Get<T>();
}

public interface ITest : ITestParent
{
new IList<T> Get<T>() where T : class, new();
}
""");

await That(result.Diagnostics).IsEmpty()
.Because("CS0452: the hiding member requires a reference type, the hidden member does not");
await That(result.Sources["Mock.ITest.g.cs"])
.Contains("""
if (this.MockRegistry.Wraps is global::MyCode.ITestParent wraps)
{
wrappedResult = wraps.Get<T>();
""").IgnoringNewlineStyle();
}

[Fact]
public async Task HiddenGenericMethod_WithNarrowedConstraints_ShouldNotDelegateToHidingMember()
{
GeneratorResult result = Generator
.Run("""
using System.Collections.Generic;
using Mockolate;

namespace MyCode;

public class Program
{
public static void Main(string[] args)
{
_ = ITest.CreateMock();
}
}

public interface ITestParent
{
IEnumerable<T> Get<T>() where T : notnull;
}

public interface ITest : ITestParent
{
new IEnumerable<T> Get<T>() where T : struct;
}
""");

await That(result.Diagnostics).IsEmpty()
.Because("CS0453: the hiding member requires a non-nullable value type, the hidden member does not");
await That(result.Sources["Mock.ITest.g.cs"])
.Contains("""
if (this.MockRegistry.Wraps is global::MyCode.ITestParent wraps)
{
wrappedResult = wraps.Get<T>();
""").IgnoringNewlineStyle();
}

[Fact]
public async Task HiddenGenericMethod_InDeepHierarchy_ShouldNotDelegateToHidingMember()
{
GeneratorResult result = Generator
.Run("""
using System.Collections.Generic;
using Mockolate;

namespace MyCode;

public class Program
{
public static void Main(string[] args)
{
_ = ITest.CreateMock();
}
}

public interface IGrandParent
{
T Get<T>(int id) where T : notnull;
}

public interface ITestParent : IGrandParent
{
new IEnumerable<T> Get<T>(int id) where T : notnull;
}

public interface ITest : ITestParent
{
new IList<T> Get<T>(int id) where T : notnull;
}
""");

await That(result.Diagnostics).IsEmpty()
.Because("CS0266: each hidden member has its own return type");
await That(result.Sources["Mock.ITest.g.cs"])
.Contains("""
if (this.MockRegistry.Wraps is global::MyCode.ITestParent wraps)
{
wrappedResult = wraps.Get<T>(id);
""").IgnoringNewlineStyle().And
.Contains("""
if (this.MockRegistry.Wraps is global::MyCode.IGrandParent wraps)
{
wrappedResult = wraps.Get<T>(id);
""").IgnoringNewlineStyle()
.Because("every level of the hierarchy must delegate to its own declaring interface");
}

[Fact]
public async Task MembersWithReservedNames_ShouldPrefixAtSymbol()
{
Expand Down
29 changes: 29 additions & 0 deletions Tests/Mockolate.Tests/MockTests.WrappingInterfaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ void Handler(string type, int amount)
}
}

[Fact]
public async Task Wrap_HiddenGenericMethod_ShouldDelegateToDeclaringInterface()
{
MyChocolateCatalog myCatalog = new();
IChocolateCatalog wrappedCatalog = IChocolateCatalog.CreateMock().Wrapping(myCatalog);

_ = wrappedCatalog.Get<string>();
_ = ((IChocolateSource)wrappedCatalog).Get<string>();

await That(myCatalog.ReceivedCalls).IsEqualTo(["catalog", "source",]);
}

[Fact]
public async Task Wrap_Indexer_ShouldDelegateToWrappedInstance()
{
Expand Down Expand Up @@ -171,6 +183,23 @@ public bool Dispense(string type, int amount)
public event ChocolateDispensedDelegate? ChocolateDispensed;
}

private class MyChocolateCatalog : IChocolateCatalog
{
public List<string> ReceivedCalls { get; } = [];

public IList<T> Get<T>() where T : notnull
{
ReceivedCalls.Add("catalog");
return [];
}

IEnumerable<T> IChocolateSource.Get<T>()
Comment thread
vbreuss marked this conversation as resolved.
{
ReceivedCalls.Add("source");
return [];
}
}

public delegate void MyDelegate();
}
}
13 changes: 13 additions & 0 deletions Tests/Mockolate.Tests/TestHelpers/IChocolateCatalog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;

namespace Mockolate.Tests.TestHelpers;

public interface IChocolateSource
{
IEnumerable<T> Get<T>() where T : notnull;
}

public interface IChocolateCatalog : IChocolateSource
{
new IList<T> Get<T>() where T : notnull;
}
Loading