Skip to content

Commit 629cee3

Browse files
authored
fix: ambiguous reference error when mocking overlapping interface combinations (#477)
Fixes ambiguous reference compiler errors by deduplicating generated setup/verify extension properties when multiple mock combinations share the same interfaces for a given base type. ### Key Changes: - Track generated (base type, interface) pairs during generation and avoid emitting duplicates. - Generate mocks in ascending “additional implementation count” order to prefer simpler combinations first. - Add tests covering both the generator output and the runtime fallback behavior in web-related matching.
1 parent 43525b7 commit 629cee3

3 files changed

Lines changed: 96 additions & 6 deletions

File tree

Source/Mockolate.SourceGenerators/MockGenerator.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,16 @@ void IIncrementalGenerator.Initialize(IncrementalGeneratorInitializationContext
3434
(spc, source) => Execute([..source.Distinct(),], spc));
3535
}
3636

37+
#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high
3738
private static void Execute(ImmutableArray<MockClass> mocksToGenerate, SourceProductionContext context)
3839
{
3940
(string Name, MockClass MockClass)[] namedMocksToGenerate = CreateNames(mocksToGenerate);
40-
foreach ((string Name, MockClass MockClass) mockToGenerate in namedMocksToGenerate)
41+
HashSet<(string? BaseNamespace, string BaseClassName, string? Namespace, string ClassName)>
42+
generatedAdditionalInterfacesByBaseType = new();
43+
44+
foreach ((string Name, MockClass MockClass) mockToGenerate in namedMocksToGenerate
45+
.OrderBy(m => m.MockClass.AdditionalImplementations.Count)
46+
.ThenBy(m => m.Name))
4147
{
4248
if (!IsValidMockDeclaration(mockToGenerate.MockClass))
4349
{
@@ -48,10 +54,20 @@ private static void Execute(ImmutableArray<MockClass> mocksToGenerate, SourcePro
4854
SourceText.From(Sources.Sources.ForMock(mockToGenerate.Name, mockToGenerate.MockClass), Encoding.UTF8));
4955
if (mockToGenerate.MockClass.AdditionalImplementations.Any() && mockToGenerate.MockClass.Delegate is null)
5056
{
51-
context.AddSource($"MockFor{mockToGenerate.Name}Extensions.g.cs",
52-
SourceText.From(
53-
Sources.Sources.ForMockCombinationExtensions(mockToGenerate.Name, mockToGenerate.MockClass, mockToGenerate.MockClass.DistinctAdditionalImplementations()),
54-
Encoding.UTF8));
57+
Class[] interfacesToGenerate = mockToGenerate.MockClass.DistinctAdditionalImplementations()
58+
.Where(impl => generatedAdditionalInterfacesByBaseType .Add(
59+
(mockToGenerate.MockClass.Namespace, mockToGenerate.MockClass.ClassName,
60+
impl.Namespace, impl.ClassName)))
61+
.ToArray();
62+
63+
if (interfacesToGenerate.Length > 0)
64+
{
65+
context.AddSource($"MockFor{mockToGenerate.Name}Extensions.g.cs",
66+
SourceText.From(
67+
Sources.Sources.ForMockCombinationExtensions(mockToGenerate.Name, mockToGenerate.MockClass,
68+
interfacesToGenerate),
69+
Encoding.UTF8));
70+
}
5571
}
5672
}
5773

@@ -118,6 +134,7 @@ private static void Execute(ImmutableArray<MockClass> mocksToGenerate, SourcePro
118134
context.AddSource("MockBehaviorExtensions.g.cs",
119135
SourceText.From(Sources.Sources.MockBehaviorExtensions(mocksToGenerate), Encoding.UTF8));
120136
}
137+
#pragma warning restore S3776 // Cognitive Complexity of methods should not be too high
121138

122139
private static List<(string Name, Class Class)> GetDistinctExtensions(ImmutableArray<MockClass> mocksToGenerate)
123140
{

Tests/Mockolate.Internal.Tests/WebTests.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,30 @@ public async Task HttpContentParameter_MatchesWithNullContent_ShouldReturnFalse(
3131
await That(result).IsFalse();
3232
}
3333

34+
[Fact]
35+
public async Task WhenParameterDoesNotImplementIHttpRequestMessagePropertyParameter_ShouldFallbackToParameterMatch()
36+
{
37+
ItExtensions.IHttpContentParameter parameter =
38+
Mock.Create<ItExtensions.IHttpContentParameter, IParameter>();
39+
parameter.SetupIParameterMock.Method.Matches(It.IsAny<object?>()).Returns(true);
40+
41+
ItExtensions.IStringContentBodyParameter sut = parameter.WithString("foo");
42+
43+
bool result = ((IHttpRequestMessagePropertyParameter<HttpContent?>)sut).Matches(null, null);
44+
45+
await That(result).IsTrue();
46+
await That(parameter.VerifyOnIParameterMock.Invoked
47+
.Matches(It.IsNull<object?>()))
48+
.Once();
49+
}
50+
3451
[Fact]
3552
public async Task WhenParameterImplementsIHttpRequestMessagePropertyParameter_ShouldUseThisMatch()
3653
{
3754
ItExtensions.IHttpContentParameter parameter =
3855
Mock.Create<ItExtensions.IHttpContentParameter, IParameter,
3956
IHttpRequestMessagePropertyParameter<HttpContent?>>();
40-
parameter.SetupIParameterMock.Method.Matches(It.IsAny<object?>()).Returns(true);
57+
parameter.SetupIParameterMock.Method.Matches(It.IsAny<object?>()).Returns(false);
4158
parameter.SetupIHttpRequestMessagePropertyParameter_HttpContent_Mock.Method
4259
.Matches(It.IsAny<HttpContent?>(), It.IsAny<HttpRequestMessage?>()).Returns(true);
4360

@@ -49,6 +66,9 @@ public async Task WhenParameterImplementsIHttpRequestMessagePropertyParameter_Sh
4966
await That(parameter.VerifyOnIHttpRequestMessagePropertyParameter_HttpContent_Mock.Invoked
5067
.Matches(It.IsNull<HttpContent?>(), It.IsNull<HttpRequestMessage?>()))
5168
.Once();
69+
await That(parameter.VerifyOnIParameterMock.Invoked
70+
.Matches(It.IsNull<object?>()))
71+
.Never();
5272
}
5373

5474
[Fact]

Tests/Mockolate.SourceGenerators.Tests/MockGeneratorTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,4 +421,57 @@ public static void Main(string[] args)
421421
await That(result.Sources).ContainsKey("MockForHttpMessageHandler.g.cs").And
422422
.ContainsKey("MockForHttpClient.g.cs");
423423
}
424+
425+
[Fact]
426+
public async Task WhenSameTypeImplementsDifferentCombinationsOfSameInterface_ShouldNotGenerateDuplicates()
427+
{
428+
GeneratorResult result = Generator
429+
.Run("""
430+
using System;
431+
using System.Threading;
432+
using System.Threading.Tasks;
433+
using Mockolate;
434+
435+
namespace MyCode
436+
{
437+
public class Program
438+
{
439+
public static void Main(string[] args)
440+
{
441+
_ = Mock.Create<IBaseInterface, ICommonInterface>();
442+
_ = Mock.Create<IBaseInterface, ICommonInterface, IAdditionalInterface1>();
443+
_ = Mock.Create<IBaseInterface, IAdditionalInterface2, ICommonInterface>();
444+
_ = Mock.Create<IBaseInterface, IAdditionalInterface1, IAdditionalInterface2, ICommonInterface>();
445+
}
446+
}
447+
448+
public interface IBaseInterface { }
449+
public interface ICommonInterface { }
450+
public interface IAdditionalInterface1 { }
451+
public interface IAdditionalInterface2 { }
452+
}
453+
""");
454+
455+
await That(result.Sources)
456+
.ContainsKey("MockForIBaseInterface_ICommonInterfaceExtensions.g.cs").And
457+
.ContainsKey("MockForIBaseInterface_ICommonInterface_IAdditionalInterface1Extensions.g.cs").And
458+
.ContainsKey("MockForIBaseInterface_IAdditionalInterface2_ICommonInterfaceExtensions.g.cs").And
459+
.DoesNotContainKey("MockForIBaseInterface_IAdditionalInterface1_IAdditionalInterface2_ICommonInterfaceExtensions.g.cs");
460+
461+
await That(result.Sources["MockForIBaseInterface_ICommonInterfaceExtensions.g.cs"])
462+
.Contains("SetupICommonInterfaceMock").And
463+
.Contains("VerifyOnICommonInterfaceMock");
464+
465+
await That(result.Sources["MockForIBaseInterface_ICommonInterface_IAdditionalInterface1Extensions.g.cs"])
466+
.DoesNotContain("SetupICommonInterfaceMock").And
467+
.DoesNotContain("VerifyOnICommonInterfaceMock").And
468+
.Contains("SetupIAdditionalInterface1Mock").And
469+
.Contains("VerifyOnIAdditionalInterface1Mock");
470+
471+
await That(result.Sources["MockForIBaseInterface_IAdditionalInterface2_ICommonInterfaceExtensions.g.cs"])
472+
.DoesNotContain("SetupICommonInterfaceMock").And
473+
.DoesNotContain("VerifyOnICommonInterfaceMock").And
474+
.Contains("SetupIAdditionalInterface2Mock").And
475+
.Contains("VerifyOnIAdditionalInterface2Mock");
476+
}
424477
}

0 commit comments

Comments
 (0)