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 @@ -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");
Expand Down
67 changes: 67 additions & 0 deletions tests/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> where T : unmanaged
{
void Fill(ref T snapshot);
}

public interface IConstrainedSource<T> where T : unmanaged, IDisposable
{
T Get();
}

public class TestUsage
{
void M()
{
var mock = Mock.Of<ISnapshotSource<int>>();
var mock2 = Mock.Of<IConstrainedSource<System.Threading.CancellationTokenRegistration>>();
}
}
""";

return VerifyGeneratorOutput(source);
}

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

public interface ISnapshotSource<T> where T : unmanaged
{
void Fill(ref T snapshot);
}

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

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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ file sealed class IConstrainedMockImpl : global::IConstrained, global::TUnit.Moc
return _engine.HandleCallWithReturn<T>(1, "GetNew", global::System.Array.Empty<object?>(), default!, global::TUnit.Mocks.TypeArguments.Of<T>.Value);
}

public T GetUnmanaged<T>() where T : struct, unmanaged
public T GetUnmanaged<T>() where T : unmanaged
{
return _engine.HandleCallWithReturn<T>(2, "GetUnmanaged", global::System.Array.Empty<object?>(), default, global::TUnit.Mocks.TypeArguments.Of<T>.Value);
}
Expand Down Expand Up @@ -133,7 +133,7 @@ public static class IConstrained_MockMemberExtensions
return new global::TUnit.Mocks.MockMethodCall<T>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "GetNew", matchers, global::TUnit.Mocks.TypeArguments.Of<T>.Value);
}

public static global::TUnit.Mocks.MockMethodCall<T> GetUnmanaged<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : struct, unmanaged
public static global::TUnit.Mocks.MockMethodCall<T> GetUnmanaged<T>(this global::TUnit.Mocks.Mock<global::IConstrained> mock) where T : 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, global::TUnit.Mocks.TypeArguments.Of<T>.Value);
Expand Down
Loading
Loading