diff --git a/src/TUnit.Mocks.SourceGenerator/Extensions/MethodSymbolExtensions.cs b/src/TUnit.Mocks.SourceGenerator/Extensions/MethodSymbolExtensions.cs index 1ef284ba65..e5c7212a04 100644 --- a/src/TUnit.Mocks.SourceGenerator/Extensions/MethodSymbolExtensions.cs +++ b/src/TUnit.Mocks.SourceGenerator/Extensions/MethodSymbolExtensions.cs @@ -36,7 +36,9 @@ public static string GetGenericConstraints(this ITypeParameterSymbol typeParam) if (typeParam.HasReferenceTypeConstraint) constraints.Add("class"); - if (typeParam.HasValueTypeConstraint) + // Roslyn reports HasValueTypeConstraint for 'unmanaged' too, but C# forbids + // combining 'struct' with 'unmanaged' (CS0449) — emit only 'unmanaged' (#6471) + if (typeParam.HasValueTypeConstraint && !typeParam.HasUnmanagedTypeConstraint) constraints.Add("struct"); if (typeParam.HasUnmanagedTypeConstraint) constraints.Add("unmanaged"); diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs index 087607c788..2315cb7060 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs @@ -492,6 +492,73 @@ void M() return VerifyGeneratorOutput(source); } + [Test] + public Task Interface_With_Unmanaged_Constrained_Type_Parameter() + { + // #6471: 'unmanaged' also sets HasValueTypeConstraint, so the generator emitted + // 'where T : struct, unmanaged' which is invalid C# (CS0449) + var source = """ + using System; + using TUnit.Mocks; + + public interface ISnapshotSource where T : unmanaged + { + void Fill(ref T snapshot); + } + + public interface IConstrainedSource where T : unmanaged, IDisposable + { + T Get(); + } + + public class TestUsage + { + void M() + { + var mock = Mock.Of>(); + var mock2 = Mock.Of>(); + } + } + """; + + return VerifyGeneratorOutput(source); + } + + [Test] + public void Interface_With_Unmanaged_Constrained_Type_Parameter_Compiles() + { + var source = """ + using System; + using TUnit.Mocks; + + public interface ISnapshotSource where T : unmanaged + { + void Fill(ref T snapshot); + } + + public class TestUsage + { + void M() + { + var mock = Mock.Of>(); + } + } + """; + + var errors = GetGeneratedCompilationErrors(source); + + // CS0449: invalid 'struct, unmanaged' combination; CS8377: cascade when T no + // longer satisfies the interface's 'unmanaged' constraint (#6471) + foreach (var id in (string[])["CS0449", "CS8377"]) + { + var match = errors.FirstOrDefault(e => string.Equals(e.Id, id, StringComparison.Ordinal)); + if (match is not null) + { + throw new InvalidOperationException($"Generated code produced {id}: {match}"); + } + } + } + [Test] public Task Interface_With_Overloaded_Methods() { diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Method_Constraints_On_Explicit_Impl.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Method_Constraints_On_Explicit_Impl.verified.txt index 47307014a3..2318debc6d 100644 --- a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Method_Constraints_On_Explicit_Impl.verified.txt +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Generic_Method_Constraints_On_Explicit_Impl.verified.txt @@ -50,7 +50,7 @@ file sealed class IConstrainedMockImpl : global::IConstrained, global::TUnit.Moc return _engine.HandleCallWithReturn(1, "GetNew", global::System.Array.Empty(), default!, global::TUnit.Mocks.TypeArguments.Of.Value); } - public T GetUnmanaged() where T : struct, unmanaged + public T GetUnmanaged() where T : unmanaged { return _engine.HandleCallWithReturn(2, "GetUnmanaged", global::System.Array.Empty(), default, global::TUnit.Mocks.TypeArguments.Of.Value); } @@ -133,7 +133,7 @@ public static class IConstrained_MockMemberExtensions return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetNew", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); } - public static global::TUnit.Mocks.MockMethodCall GetUnmanaged(this global::TUnit.Mocks.Mock mock) where T : struct, unmanaged + public static global::TUnit.Mocks.MockMethodCall GetUnmanaged(this global::TUnit.Mocks.Mock mock) where T : unmanaged { var matchers = global::System.Array.Empty(); return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 2, "GetUnmanaged", matchers, global::TUnit.Mocks.TypeArguments.Of.Value); diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unmanaged_Constrained_Type_Parameter.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unmanaged_Constrained_Type_Parameter.verified.txt new file mode 100644 index 0000000000..a3ce482315 --- /dev/null +++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Unmanaged_Constrained_Type_Parameter.verified.txt @@ -0,0 +1,404 @@ +// +#pragma warning disable +#nullable enable + +public sealed class IConstrainedSource_T_Mock : global::TUnit.Mocks.Mock>, global::IConstrainedSource where T : unmanaged, global::System.IDisposable +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal IConstrainedSource_T_Mock(global::IConstrainedSource mockObject, global::TUnit.Mocks.MockEngine> engine) + : base(mockObject, engine) { } + + T global::IConstrainedSource.Get() => Object.Get(); +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +file sealed class IConstrainedSource_T_MockImpl : global::IConstrainedSource, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject where T : unmanaged, global::System.IDisposable +{ + private readonly global::TUnit.Mocks.MockEngine> _engine; + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + + internal IConstrainedSource_T_MockImpl(global::TUnit.Mocks.MockEngine> engine) + { + _engine = engine; + } + + public T Get() + { + return _engine.HandleCallWithReturn(0, "Get", global::System.Array.Empty(), 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."); + } +} + +internal static class IConstrainedSource_T_MockFactory +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterOpenGenericFactory( + typeof(global::IConstrainedSource<>), + typeof(IConstrainedSource_T_MockImpl<>), + typeof(IConstrainedSource_T_Mock<>)); + } + + internal static global::TUnit.Mocks.Mock> CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior) where T : unmanaged, global::System.IDisposable + { + var engine = new global::TUnit.Mocks.MockEngine>(behavior); + var impl = new IConstrainedSource_T_MockImpl(engine); + engine.Raisable = impl; + var mock = new IConstrainedSource_T_Mock(impl, engine); + return mock; + } + +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +public static class IConstrainedSource_T__MockMemberExtensions +{ + public static global::TUnit.Mocks.MockMethodCall Get(this global::TUnit.Mocks.Mock> mock) where T : unmanaged, global::System.IDisposable + { + var matchers = global::System.Array.Empty(); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Get", matchers); + } + + #if NET9_0_OR_GREATER + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void Reset(this global::TUnit.Mocks.Mock> mock) where T : unmanaged, global::System.IDisposable + => global::TUnit.Mocks.Mock.Reset(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void VerifyAll(this global::TUnit.Mocks.Mock> mock) where T : unmanaged, global::System.IDisposable + => global::TUnit.Mocks.Mock.VerifyAll(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void VerifyNoOtherCalls(this global::TUnit.Mocks.Mock> mock) where T : unmanaged, global::System.IDisposable + => global::TUnit.Mocks.Mock.VerifyNoOtherCalls(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void SetupAllProperties(this global::TUnit.Mocks.Mock> mock) where T : unmanaged, global::System.IDisposable + => global::TUnit.Mocks.Mock.SetupAllProperties(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static global::TUnit.Mocks.Diagnostics.MockDiagnostics GetDiagnostics(this global::TUnit.Mocks.Mock> mock) where T : unmanaged, global::System.IDisposable + => global::TUnit.Mocks.Mock.GetDiagnostics(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void SetState(this global::TUnit.Mocks.Mock> mock, string? stateName) where T : unmanaged, global::System.IDisposable + => global::TUnit.Mocks.Mock.SetState(mock, stateName); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void InState(this global::TUnit.Mocks.Mock> mock, string stateName, global::System.Action>> configure) where T : unmanaged, global::System.IDisposable + => global::TUnit.Mocks.Mock.InState(mock, stateName, configure); + + extension(global::TUnit.Mocks.Mock> mock) where T : unmanaged, global::System.IDisposable + { + public global::System.Collections.Generic.IReadOnlyList Invocations => global::TUnit.Mocks.Mock.Invocations(mock); + + public global::TUnit.Mocks.MockBehavior Behavior => global::TUnit.Mocks.Mock.Behavior(mock); + + public global::TUnit.Mocks.IDefaultValueProvider? DefaultValueProvider + { + get => global::TUnit.Mocks.Mock.GetDefaultValueProvider(mock); + set => global::TUnit.Mocks.Mock.SetDefaultValueProvider(mock, value); + } + } + #endif +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks +{ + public static class IConstrainedSource_T__MockStaticExtension + { + extension(global::IConstrainedSource _) where T : unmanaged, global::System.IDisposable + { + public static global::IConstrainedSource_T_Mock Mock() + { + return (global::IConstrainedSource_T_Mock)global::IConstrainedSource_T_MockFactory.CreateAutoMock(global::TUnit.Mocks.Mock.DefaultBehavior); + } + + public static global::IConstrainedSource_T_Mock Mock(global::TUnit.Mocks.MockBehavior behavior) + { + return (global::IConstrainedSource_T_Mock)global::IConstrainedSource_T_MockFactory.CreateAutoMock(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +public sealed class ISnapshotSource_T_Mock : global::TUnit.Mocks.Mock>, global::ISnapshotSource where T : unmanaged +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal ISnapshotSource_T_Mock(global::ISnapshotSource mockObject, global::TUnit.Mocks.MockEngine> engine) + : base(mockObject, engine) { } + + void global::ISnapshotSource.Fill(ref T snapshot) + { + Object.Fill(ref snapshot); + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +file sealed class ISnapshotSource_T_MockImpl : global::ISnapshotSource, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject where T : unmanaged +{ + private readonly global::TUnit.Mocks.MockEngine> _engine; + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; } + + internal ISnapshotSource_T_MockImpl(global::TUnit.Mocks.MockEngine> engine) + { + _engine = engine; + } + + public void Fill(ref T snapshot) + { + _engine.HandleCall(0, "Fill", snapshot); + var __outRef = global::TUnit.Mocks.Setup.OutRefContext.Consume(); + if (__outRef is not null) + { + if (__outRef.TryGetValue(0, out var __v0)) snapshot = (T)__v0!; + } + } + + [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."); + } +} + +internal static class ISnapshotSource_T_MockFactory +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterOpenGenericFactory( + typeof(global::ISnapshotSource<>), + typeof(ISnapshotSource_T_MockImpl<>), + typeof(ISnapshotSource_T_Mock<>)); + } + + internal static global::TUnit.Mocks.Mock> CreateAutoMock(global::TUnit.Mocks.MockBehavior behavior) where T : unmanaged + { + var engine = new global::TUnit.Mocks.MockEngine>(behavior); + var impl = new ISnapshotSource_T_MockImpl(engine); + engine.Raisable = impl; + var mock = new ISnapshotSource_T_Mock(impl, engine); + return mock; + } + +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +public static class ISnapshotSource_T__MockMemberExtensions +{ + public static ISnapshotSource_T__Fill_M0_MockCall Fill(this global::TUnit.Mocks.Mock> mock, global::TUnit.Mocks.Arguments.Arg snapshot) where T : unmanaged + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { snapshot.Matcher }; + return new ISnapshotSource_T__Fill_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Fill", matchers); + } + + #if NET9_0_OR_GREATER + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void Reset(this global::TUnit.Mocks.Mock> mock) where T : unmanaged + => global::TUnit.Mocks.Mock.Reset(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void VerifyAll(this global::TUnit.Mocks.Mock> mock) where T : unmanaged + => global::TUnit.Mocks.Mock.VerifyAll(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void VerifyNoOtherCalls(this global::TUnit.Mocks.Mock> mock) where T : unmanaged + => global::TUnit.Mocks.Mock.VerifyNoOtherCalls(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void SetupAllProperties(this global::TUnit.Mocks.Mock> mock) where T : unmanaged + => global::TUnit.Mocks.Mock.SetupAllProperties(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static global::TUnit.Mocks.Diagnostics.MockDiagnostics GetDiagnostics(this global::TUnit.Mocks.Mock> mock) where T : unmanaged + => global::TUnit.Mocks.Mock.GetDiagnostics(mock); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void SetState(this global::TUnit.Mocks.Mock> mock, string? stateName) where T : unmanaged + => global::TUnit.Mocks.Mock.SetState(mock, stateName); + + [global::System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static void InState(this global::TUnit.Mocks.Mock> mock, string stateName, global::System.Action>> configure) where T : unmanaged + => global::TUnit.Mocks.Mock.InState(mock, stateName, configure); + + extension(global::TUnit.Mocks.Mock> mock) where T : unmanaged + { + public global::System.Collections.Generic.IReadOnlyList Invocations => global::TUnit.Mocks.Mock.Invocations(mock); + + public global::TUnit.Mocks.MockBehavior Behavior => global::TUnit.Mocks.Mock.Behavior(mock); + + public global::TUnit.Mocks.IDefaultValueProvider? DefaultValueProvider + { + get => global::TUnit.Mocks.Mock.GetDefaultValueProvider(mock); + set => global::TUnit.Mocks.Mock.SetDefaultValueProvider(mock, value); + } + } + #endif +} + +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +public sealed class ISnapshotSource_T__Fill_M0_MockCall : global::TUnit.Mocks.Verification.ICallVerification where T : unmanaged +{ + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + + internal ISnapshotSource_T__Fill_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + _ = EnsureSetup(); + } + + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() + { + var existing = global::System.Threading.Volatile.Read(ref _builder); + if (existing is not null) return existing; + return EnsureSetupSlow(); + } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetupSlow() + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + var fresh = new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + var prev = global::System.Threading.Interlocked.CompareExchange(ref _builder, fresh, null); + if (prev is not null) return prev; + // AddSetup runs only on the CAS winner. Setup is sequential in practice, + // so a concurrent loser observing the builder before registration is benign. + _engine.AddSetup(setup); + return fresh; + } + + /// + public ISnapshotSource_T__Fill_M0_MockCall Returns() { EnsureSetup().Returns(); return this; } + /// + public ISnapshotSource_T__Fill_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public ISnapshotSource_T__Fill_M0_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public ISnapshotSource_T__Fill_M0_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public ISnapshotSource_T__Fill_M0_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public ISnapshotSource_T__Fill_M0_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Execute a typed callback using the actual method parameters. + public ISnapshotSource_T__Fill_M0_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(callback); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public ISnapshotSource_T__Fill_M0_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((T)args[0]!)); + return this; + } + + /// Sets the 'snapshot' ref parameter to the specified value when this setup matches. + public ISnapshotSource_T__Fill_M0_MockCall SetsRefSnapshot(T snapshot) { EnsureSetup().SetsOutParameter(0, snapshot); return this; } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks +{ + public static class ISnapshotSource_T__MockStaticExtension + { + extension(global::ISnapshotSource _) where T : unmanaged + { + public static global::ISnapshotSource_T_Mock Mock() + { + return (global::ISnapshotSource_T_Mock)global::ISnapshotSource_T_MockFactory.CreateAutoMock(global::TUnit.Mocks.Mock.DefaultBehavior); + } + + public static global::ISnapshotSource_T_Mock Mock(global::TUnit.Mocks.MockBehavior behavior) + { + return (global::ISnapshotSource_T_Mock)global::ISnapshotSource_T_MockFactory.CreateAutoMock(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#pragma warning disable +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file