diff --git a/Source/Mockolate.SourceGenerators/Entities/GenericParameter.cs b/Source/Mockolate.SourceGenerators/Entities/GenericParameter.cs index 16f67d46..36a4f1e6 100644 --- a/Source/Mockolate.SourceGenerators/Entities/GenericParameter.cs +++ b/Source/Mockolate.SourceGenerators/Entities/GenericParameter.cs @@ -33,11 +33,24 @@ public GenericParameter(ITypeParameterSymbol typeSymbol) public bool IsClass { get; } #pragma warning disable S3776 // Cognitive Complexity of methods should not be too high - public void AppendWhereConstraint(StringBuilder sb, string prefix) + public void AppendWhereConstraint(StringBuilder sb, string prefix, bool isExplicitInterfaceImpl = false) { - if (!ConstraintTypes.Any() && !IsStruct && !IsClass && !IsNotNull && !IsUnmanaged && !HasConstructor && - !AllowsRefStruct) + bool isUnconstrained = !ConstraintTypes.Any() && !IsStruct && !IsClass && !IsNotNull && !IsUnmanaged && + !HasConstructor && !AllowsRefStruct; + + if (isUnconstrained) + { + if (isExplicitInterfaceImpl) + { + sb.AppendLine().Append(prefix).Append("where ").Append(Name).Append(" : default"); + } + + return; + } + + if (isExplicitInterfaceImpl) { + // Constrained type parameters are inherited in explicit interface implementations (CS0460) return; } diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index 34a29cdb..d83af1d6 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -1034,6 +1034,7 @@ private static void AppendMockSubject_ImplementClass_AddEvent(StringBuilder sb, .Append("? ").Append(backingFieldName).Append(';').AppendLine(); backingFieldAccess = $"this.{backingFieldName}"; } + sb.Append("\t\t/// "); sb.Append(@event.Attributes, "\t\t"); @@ -1499,7 +1500,7 @@ private static void AppendMockSubject_ImplementClass_AddMethod(StringBuilder sb, { foreach (GenericParameter gp in method.GenericParameters.Value) { - gp.AppendWhereConstraint(sb, "\t\t\t"); + gp.AppendWhereConstraint(sb, "\t\t\t", explicitInterfaceImplementation); } } @@ -1797,7 +1798,7 @@ private static void DefineSetupInterface(StringBuilder sb, Class @class, MemberT { foreach (bool[] valueFlags in GenerateValueFlagCombinations(indexer.IndexerParameters.Value)) { - AppendIndexerSetupDefinition(sb, indexer, valueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority); + AppendIndexerSetupDefinition(sb, indexer, valueFlags, hasOverloadResolutionPriority); } } else @@ -1805,7 +1806,7 @@ private static void DefineSetupInterface(StringBuilder sb, Class @class, MemberT bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanUseNullableParameterOverload()).ToArray(); if (allValueFlags.Any(f => f)) { - AppendIndexerSetupDefinition(sb, indexer, allValueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority); + AppendIndexerSetupDefinition(sb, indexer, allValueFlags, hasOverloadResolutionPriority); } } } @@ -2216,6 +2217,14 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met sb.Append(")"); } + if (method.GenericParameters is not null && method.GenericParameters.Value.Count > 0) + { + foreach (GenericParameter gp in method.GenericParameters.Value) + { + gp.AppendWhereConstraint(sb, "\t\t\t", true); + } + } + sb.AppendLine(); sb.AppendLine("\t\t{"); if (method.ReturnType != Type.Void) @@ -2630,7 +2639,7 @@ private static void DefineVerifyInterface(StringBuilder sb, Class @class, string { foreach (bool[] valueFlags in GenerateValueFlagCombinations(indexer.IndexerParameters.Value)) { - AppendIndexerVerifyDefinition(sb, indexer, verifyName, valueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority); + AppendIndexerVerifyDefinition(sb, indexer, verifyName, valueFlags, hasOverloadResolutionPriority); } } else @@ -2638,7 +2647,7 @@ private static void DefineVerifyInterface(StringBuilder sb, Class @class, string bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanUseNullableParameterOverload()).ToArray(); if (allValueFlags.Any(f => f)) { - AppendIndexerVerifyDefinition(sb, indexer, verifyName, allValueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority); + AppendIndexerVerifyDefinition(sb, indexer, verifyName, allValueFlags, hasOverloadResolutionPriority); } } } @@ -2955,6 +2964,14 @@ private static void AppendMethodVerifyImplementation(StringBuilder sb, Method me } sb.Append(")"); + if (method.GenericParameters is not null && method.GenericParameters.Value.Count > 0) + { + foreach (GenericParameter gp in method.GenericParameters.Value) + { + gp.AppendWhereConstraint(sb, "\t\t\t", true); + } + } + sb.AppendLine(); sb.Append("\t\t\t=> this.").Append(mockRegistryName).Append(".Method<").Append(verifyName).Append(">(this, "); diff --git a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs index bf71760d..84ce39f9 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs @@ -6,6 +6,43 @@ public sealed partial class ClassTests { public sealed class MethodTests { + [Fact] + public async Task ExplicitInterfaceImplementation_WithUnconstrainedGeneric_ShouldHaveDefaultConstraint() + { + GeneratorResult result = Generator + .Run(""" + using System.Threading.Tasks; + using Mockolate; + + namespace MyCode; + public class Program + { + public static void Main(string[] args) + { + _ = IMyService.CreateMock().Implementing(); + } + } + + public interface IMyService + { + void MyMethod(); + } + + public interface IMyOtherService + { + Task DoSomethingAsync(); + } + """); + + await That(result.Sources).ContainsKey("Mock.IMyService__IMyOtherService.g.cs").WhoseValue + .Contains(""" + /// + global::System.Threading.Tasks.Task global::MyCode.IMyOtherService.DoSomethingAsync() + where T : default + { + """).IgnoringNewlineStyle(); + } + [Theory] [InlineData("class, T")] [InlineData("struct")] diff --git a/Tests/Mockolate.Tests/MockTests.cs b/Tests/Mockolate.Tests/MockTests.cs index 5b0c0440..8a6ddc74 100644 --- a/Tests/Mockolate.Tests/MockTests.cs +++ b/Tests/Mockolate.Tests/MockTests.cs @@ -152,7 +152,7 @@ public async Task Create_WithRequiredParameters_WithEmptyParameters_ShouldThrowM { void Act() { - _ = MyBaseClassWithConstructor.CreateMock(constructorParameters: []); + _ = MyBaseClassWithConstructor.CreateMock([]); } await That(Act).Throws() @@ -190,11 +190,11 @@ public async Task Create_WithSetups_ShouldAllowChangingTheSetupSubjectInCallback public async Task Create_WithSetups_ShouldApplySetups() { IMyService mock = IMyService.CreateMock(setup => - { - setup.Multiply(It.Is(1), It.IsAny()).Returns(2); - setup.Multiply(It.Is(2), It.IsAny()).Returns(4); - setup.Multiply(It.Is(3), It.IsAny()).Returns(8); - }); + { + setup.Multiply(It.Is(1), It.IsAny()).Returns(2); + setup.Multiply(It.Is(2), It.IsAny()).Returns(4); + setup.Multiply(It.Is(3), It.IsAny()).Returns(8); + }); int result1 = mock.Multiply(1, null); int result2 = mock.Multiply(2, null); @@ -242,6 +242,21 @@ public async Task GenericMethodWithWhereClause_ShouldWork() await That(sut.Mock.Verify.MyMethod(It.IsTrue())).Once(); } + [Fact] + public async Task GenericMethodWithWhereClause_WhenImplementingAdditionalInterface_ShouldWork() + { + IChocolateDispenser sut = IChocolateDispenser.CreateMock() + .Implementing(); + IMyServiceWithGenericMethodsWithWhereClause service = (IMyServiceWithGenericMethodsWithWhereClause)sut; + + sut.Mock.As().Setup.MyMethod(It.IsTrue()).Returns(3); + + int result = service.MyMethod(true); + + await That(result).IsEqualTo(3); + await That(sut.Mock.As().Verify.MyMethod(It.IsTrue())).Once(); + } + [Fact] public async Task ToString_ShouldReturnImplementedType() { @@ -262,6 +277,17 @@ public async Task ToString_WithAdditionalImplementations_ShouldReturnImplemented await That(result).IsEqualTo("Mockolate.Tests.TestHelpers.IChocolateDispenser mock that also implements Mockolate.Tests.MockTests.IMyService"); } + [Fact] + public async Task TypeWithMockRegistryMembers_ShouldUseUniqueName() + { + IServiceWithMockRegistryMembers sut = IServiceWithMockRegistryMembers.CreateMock(); + sut.Mock.Setup.MockRegistry_1.Returns("foo"); + + string result = sut.MockRegistry_1; + + await That(result).IsEqualTo("foo"); + } + [Fact] public async Task WhenTypeHasMockAndMockolate_MockProperty_ShouldAppendNumbers() { @@ -308,17 +334,6 @@ public async Task WithoutConstructorParameters_MockConstructorParametersShouldBe await That(mock.MockRegistry.ConstructorParameters).IsNull(); } - [Fact] - public async Task TypeWithMockRegistryMembers_ShouldUseUniqueName() - { - IServiceWithMockRegistryMembers sut = IServiceWithMockRegistryMembers.CreateMock(); - sut.Mock.Setup.MockRegistry_1.Returns("foo"); - - string result = sut.MockRegistry_1; - - await That(result).IsEqualTo("foo"); - } - public interface MyInterfaceWithMockProperty { int Mock { get; } @@ -403,10 +418,10 @@ public interface IMyServiceWithGenericMethodsWithWhereClause public interface IServiceWithMockRegistryMembers { - event EventHandler MockRegistry; - int MockolateMockRegistry(bool value); // ReSharper disable once InconsistentNaming string MockRegistry_1 { get; } + event EventHandler MockRegistry; + int MockolateMockRegistry(bool value); } public sealed class Nested