Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
29 changes: 29 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,35 @@ void M()
return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_Generic_Method_Constraints_On_Explicit_Impl()
{
var source = """
using System;
using TUnit.Mocks;

public interface IConstrained
{
T GetNotnull<T>(string key) where T : notnull;
T GetNew<T>() where T : new();
T GetUnmanaged<T>() where T : unmanaged;
T GetDisposable<T>() where T : IDisposable;
T GetClassNew<T>() where T : class, IDisposable, new();
T GetStructDisposable<T>() where T : struct, IDisposable;
}

public class TestUsage
{
void M()
{
var mock = Mock.Of<IConstrained>();
}
}
""";

return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_Overloaded_Methods()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
public sealed class IConstrainedMock : global::TUnit.Mocks.Mock<global::IConstrained>, global::IConstrained
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal IConstrainedMock(global::IConstrained mockObject, global::TUnit.Mocks.MockEngine<global::IConstrained> engine)
: base(mockObject, engine) { }

T global::IConstrained.GetNotnull<T>(string key) => Object.GetNotnull<T>(key);

T global::IConstrained.GetNew<T>() => Object.GetNew<T>();

T global::IConstrained.GetUnmanaged<T>() where T : struct => Object.GetUnmanaged<T>();

T global::IConstrained.GetDisposable<T>() => Object.GetDisposable<T>();

T global::IConstrained.GetClassNew<T>() where T : class => Object.GetClassNew<T>();

T global::IConstrained.GetStructDisposable<T>() where T : struct => Object.GetStructDisposable<T>();
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
file sealed class IConstrainedMockImpl : global::IConstrained, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
{
private readonly global::TUnit.Mocks.MockEngine<global::IConstrained> _engine;

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; }

internal IConstrainedMockImpl(global::TUnit.Mocks.MockEngine<global::IConstrained> engine)
{
_engine = engine;
}

public T GetNotnull<T>(string key) where T : notnull
{
return _engine.HandleCallWithReturn<T, string>(0, "GetNotnull", key, default!);
}

public T GetNew<T>() where T : new()
{
return _engine.HandleCallWithReturn<T>(1, "GetNew", global::System.Array.Empty<object?>(), default!);
}

public T GetUnmanaged<T>() where T : struct, unmanaged
{
return _engine.HandleCallWithReturn<T>(2, "GetUnmanaged", global::System.Array.Empty<object?>(), default);
}

public T GetDisposable<T>() where T : global::System.IDisposable
{
return _engine.HandleCallWithReturn<T>(3, "GetDisposable", global::System.Array.Empty<object?>(), default!);
}

public T GetClassNew<T>() where T : class, global::System.IDisposable, new()
{
return _engine.HandleCallWithReturn<T>(4, "GetClassNew", global::System.Array.Empty<object?>(), default!);
}

public T GetStructDisposable<T>() where T : struct, global::System.IDisposable
{
return _engine.HandleCallWithReturn<T>(5, "GetStructDisposable", global::System.Array.Empty<object?>(), default);
}

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public void RaiseEvent(string eventName, object? args)
{
throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock.");
}
}

file static class IConstrainedMockFactory
{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register()
{
global::TUnit.Mocks.MockRegistry.RegisterFactory<global::IConstrained>(Create);
}

internal static global::TUnit.Mocks.Mock<global::IConstrained> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::IConstrained' does not support constructor arguments, but {constructorArgs.Length} were provided.");
var engine = new global::TUnit.Mocks.MockEngine<global::IConstrained>(behavior);
var impl = new IConstrainedMockImpl(engine);
engine.Raisable = impl;
var mock = new IConstrainedMock(impl, engine);
return mock;
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
{
public static class IConstrained_MockMemberExtensions
{
public static global::TUnit.Mocks.MockMethodCall<T> GetNotnull<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock, global::TUnit.Mocks.Arguments.Arg<string> key) where T : notnull
{
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { key.Matcher };
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetNotnull", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetNotnull<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock, global::System.Func<string, bool> key) where T : notnull
{
global::TUnit.Mocks.Arguments.Arg<string> __fa_key = key;
var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_key.Matcher };
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetNotnull", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetNew<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : new()
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetNew", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetUnmanaged<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : struct, unmanaged
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "GetUnmanaged", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetDisposable<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : global::System.IDisposable
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 3, "GetDisposable", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetClassNew<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : class, global::System.IDisposable, new()
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 4, "GetClassNew", matchers);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetStructDisposable<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : struct, global::System.IDisposable
{
var matchers = global::System.Array.Empty<global::TUnit.Mocks.Arguments.IArgumentMatcher>();
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 5, "GetStructDisposable", matchers);
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks
{
public static class IConstrained_MockStaticExtension
{
extension(global::IConstrained)
{
public static global::TUnit.Mocks.Generated.IConstrainedMock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose)
{
return (global::TUnit.Mocks.Generated.IConstrainedMock)global::TUnit.Mocks.Mock.Of<global::IConstrained>(behavior);
}
}
}
}


// ===== FILE SEPARATOR =====

// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated;
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ namespace TUnit.Mocks.Generated

T global::IRepository.GetById<T>(int id) where T : class => Object.GetById<T>(id);

void global::IRepository.Save<T>(T entity) where T : class, new()
void global::IRepository.Save<T>(T entity) where T : class
{
Object.Save<T>(entity);
}

TResult global::IRepository.Transform<TInput, TResult>(TInput input) where TInput : notnull where TResult : struct => Object.Transform<TInput, TResult>(input);
TResult global::IRepository.Transform<TInput, TResult>(TInput input) where TResult : struct => Object.Transform<TInput, TResult>(input);
}
}

Expand Down
20 changes: 16 additions & 4 deletions TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,13 +1112,25 @@ private static string FormatConstraintClauses(EquatableArray<MockTypeParameterMo
var clauses = new List<string>();
foreach (var tp in typeParameters)
{
if (!string.IsNullOrEmpty(tp.Constraints))
if (forExplicitImplementation)
{
clauses.Add($"where {tp.Name} : {tp.Constraints}");
// CS0460: Only 'class' and 'struct' constraints are allowed on explicit interface implementations.
if (tp.HasReferenceTypeConstraint)
{
clauses.Add($"where {tp.Name} : class");
}
else if (tp.HasValueTypeConstraint)
{
clauses.Add($"where {tp.Name} : struct");
}
else if (tp.HasAnnotatedNullableUsage)
{
clauses.Add($"where {tp.Name} : default");
}
}
else if (forExplicitImplementation && tp.HasAnnotatedNullableUsage)
else if (!string.IsNullOrEmpty(tp.Constraints))
{
clauses.Add($"where {tp.Name} : default");
clauses.Add($"where {tp.Name} : {tp.Constraints}");
}
}
return clauses.Count > 0 ? " " + string.Join(' ', clauses) : "";
Expand Down
2 changes: 2 additions & 0 deletions TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m
{
Name = tp.Name,
Constraints = tp.GetGenericConstraints(),
HasReferenceTypeConstraint = tp.HasReferenceTypeConstraint,
HasValueTypeConstraint = tp.HasValueTypeConstraint,
HasAnnotatedNullableUsage = tp.IsUnconstrainedWithNullableUsage(method)
}).ToImmutableArray()
),
Expand Down
10 changes: 9 additions & 1 deletion TUnit.Mocks.SourceGenerator/Models/MockTypeParameterModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ internal sealed record MockTypeParameterModel : IEquatable<MockTypeParameterMode
{
public string Name { get; init; } = "";
public string Constraints { get; init; } = "";
public bool HasReferenceTypeConstraint { get; init; }
public bool HasValueTypeConstraint { get; init; }
public bool HasAnnotatedNullableUsage { get; init; }

public bool Equals(MockTypeParameterModel? other)
{
if (other is null) return false;
return Name == other.Name && Constraints == other.Constraints && HasAnnotatedNullableUsage == other.HasAnnotatedNullableUsage;
return Name == other.Name
&& Constraints == other.Constraints
&& HasReferenceTypeConstraint == other.HasReferenceTypeConstraint
&& HasValueTypeConstraint == other.HasValueTypeConstraint
&& HasAnnotatedNullableUsage == other.HasAnnotatedNullableUsage;
}

public override int GetHashCode()
Expand All @@ -21,6 +27,8 @@ public override int GetHashCode()
int hash = 17;
hash = hash * 31 + Name.GetHashCode();
hash = hash * 31 + Constraints.GetHashCode();
hash = hash * 31 + HasReferenceTypeConstraint.GetHashCode();
hash = hash * 31 + HasValueTypeConstraint.GetHashCode();
hash = hash * 31 + HasAnnotatedNullableUsage.GetHashCode();
return hash;
}
Expand Down
Loading