Skip to content

Commit 74753ac

Browse files
authored
fix: preserve nullable annotation on generic return type in setup interface (#723)
A `T?` return on a generic method whose `T` carries a non-value-type constraint (`class`, `class?`, an interface, or `notnull`) cannot be expressed in the explicit setup-interface implementation: CS0460 forbids restating the inherited constraint and `where T : default` (CS8822) conflicts with those constraints. Without a clause the compiler resolves bare `T?` as `Nullable<T>` and reports CS0453/CS9334/CS0738/CS0266. The setup-side return type now drops the trailing `?` for these methods. NRT annotations are erased at runtime, so the underlying setup object is unchanged. The user-facing mock body keeps `T?` because the constraint is visible there.
1 parent 16693da commit 74753ac

1 file changed

Lines changed: 64 additions & 4 deletions

File tree

Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,64 @@ private static bool IsFastBufferEligibleMethod(Method method)
15551555
return true;
15561556
}
15571557

1558+
/// <summary>
1559+
/// A <c>T?</c> return where <c>T</c> is one of the method's generic parameters and is
1560+
/// constrained to a reference type (or any other non-value-type constraint such as
1561+
/// <c>class</c>, <c>class?</c>, an interface, or <c>notnull</c>) cannot be expressed in
1562+
/// the explicit setup-interface implementation: CS0460 forbids restating the inherited
1563+
/// constraint, and <c>where T : default</c> (CS8822) conflicts with those constraints.
1564+
/// Without a constraint clause the compiler resolves the bare <c>T?</c> as
1565+
/// <c>Nullable&lt;T&gt;</c> and reports CS0453/CS9334/CS0738/CS0266.
1566+
///
1567+
/// The fix is to drop the trailing <c>?</c> from the setup-side return type
1568+
/// (<c>IReturnMethodSetup&lt;T&gt;</c> instead of <c>IReturnMethodSetup&lt;T?&gt;</c>) and from
1569+
/// the matching <c>ReturnMethodSetup&lt;T&gt;</c> construction. NRT annotations are erased at
1570+
/// runtime, so the underlying setup object is identical and the fluent API still composes.
1571+
/// The user-facing mock body keeps <c>T?</c> because the constraint is visible there.
1572+
/// </summary>
1573+
private static bool ShouldStripNullableGenericReturnAnnotation(Method method)
1574+
{
1575+
if (method.GenericParameters is null || method.GenericParameters.Value.Count == 0)
1576+
{
1577+
return false;
1578+
}
1579+
1580+
string fullname = method.ReturnType.Fullname;
1581+
if (fullname.Length < 2 || fullname[fullname.Length - 1] != '?')
1582+
{
1583+
return false;
1584+
}
1585+
1586+
string raw = fullname.Substring(0, fullname.Length - 1);
1587+
foreach (GenericParameter gp in method.GenericParameters.Value)
1588+
{
1589+
if (gp.Name == raw)
1590+
{
1591+
return !gp.IsStruct && !gp.IsUnmanaged;
1592+
}
1593+
}
1594+
1595+
return false;
1596+
}
1597+
1598+
/// <summary>
1599+
/// Emits the method's return type as it should appear inside the setup-side surface
1600+
/// (the <c>IReturnMethodSetup&lt;...&gt;</c> wrapper on the setup interface, the explicit
1601+
/// impl, and the <c>new ReturnMethodSetup&lt;...&gt;</c> construction). Strips a trailing
1602+
/// <c>?</c> when <see cref="ShouldStripNullableGenericReturnAnnotation" /> applies.
1603+
/// </summary>
1604+
private static void AppendSetupReturnType(StringBuilder sb, Method method)
1605+
{
1606+
if (ShouldStripNullableGenericReturnAnnotation(method))
1607+
{
1608+
string fullname = method.ReturnType.Fullname;
1609+
sb.Append(fullname, 0, fullname.Length - 1);
1610+
return;
1611+
}
1612+
1613+
sb.AppendTypeOrWrapper(method.ReturnType);
1614+
}
1615+
15581616
#pragma warning disable S107 // Methods should not have too many parameters
15591617
private static void ImplementMockForInterface(StringBuilder sb, string mockRegistryName, string name,
15601618
bool hasEvents, bool hasProtectedMembers, bool hasProtectedEvents, bool hasStaticMembers, bool hasStaticEvents)
@@ -3913,7 +3971,8 @@ private static void AppendMethodSetupDefinition(StringBuilder sb, Class @class,
39133971
: "\t\tglobal::Mockolate.Setup.IReturnMethodSetup");
39143972
}
39153973

3916-
sb.Append('<').AppendTypeOrWrapper(method.ReturnType);
3974+
sb.Append('<');
3975+
AppendSetupReturnType(sb, method);
39173976
foreach (MethodParameter parameter in method.Parameters)
39183977
{
39193978
sb.Append(", ").AppendTypeOrWrapper(parameter.Type);
@@ -4208,7 +4267,8 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met
42084267
: "\t\tglobal::Mockolate.Setup.IReturnMethodSetup");
42094268
}
42104269

4211-
sb.Append('<').AppendTypeOrWrapper(method.ReturnType);
4270+
sb.Append('<');
4271+
AppendSetupReturnType(sb, method);
42124272
foreach (MethodParameter parameter in method.Parameters)
42134273
{
42144274
sb.Append(", ").AppendTypeOrWrapper(parameter.Type);
@@ -4299,8 +4359,8 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met
42994359
string methodSetupVar = Helpers.GetUniqueLocalVariableName("methodSetup", method.Parameters);
43004360
if (method.ReturnType != Type.Void)
43014361
{
4302-
sb.Append("\t\t\tvar ").Append(methodSetupVar).Append(" = new global::Mockolate.Setup.ReturnMethodSetup<")
4303-
.AppendTypeOrWrapper(method.ReturnType);
4362+
sb.Append("\t\t\tvar ").Append(methodSetupVar).Append(" = new global::Mockolate.Setup.ReturnMethodSetup<");
4363+
AppendSetupReturnType(sb, method);
43044364

43054365
foreach (MethodParameter parameter in method.Parameters)
43064366
{

0 commit comments

Comments
 (0)