diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index 6302a188..b7b55a8d 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -1555,6 +1555,64 @@ private static bool IsFastBufferEligibleMethod(Method method) return true; } + /// + /// A T? return where T is one of the method's generic parameters and is + /// constrained to a reference type (or any other non-value-type constraint such as + /// 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 constraint clause the compiler resolves the bare T? as + /// Nullable<T> and reports CS0453/CS9334/CS0738/CS0266. + /// + /// The fix is to drop the trailing ? from the setup-side return type + /// (IReturnMethodSetup<T> instead of IReturnMethodSetup<T?>) and from + /// the matching ReturnMethodSetup<T> construction. NRT annotations are erased at + /// runtime, so the underlying setup object is identical and the fluent API still composes. + /// The user-facing mock body keeps T? because the constraint is visible there. + /// + private static bool ShouldStripNullableGenericReturnAnnotation(Method method) + { + if (method.GenericParameters is null || method.GenericParameters.Value.Count == 0) + { + return false; + } + + string fullname = method.ReturnType.Fullname; + if (fullname.Length < 2 || fullname[fullname.Length - 1] != '?') + { + return false; + } + + string raw = fullname.Substring(0, fullname.Length - 1); + foreach (GenericParameter gp in method.GenericParameters.Value) + { + if (gp.Name == raw) + { + return !gp.IsStruct && !gp.IsUnmanaged; + } + } + + return false; + } + + /// + /// Emits the method's return type as it should appear inside the setup-side surface + /// (the IReturnMethodSetup<...> wrapper on the setup interface, the explicit + /// impl, and the new ReturnMethodSetup<...> construction). Strips a trailing + /// ? when applies. + /// + private static void AppendSetupReturnType(StringBuilder sb, Method method) + { + if (ShouldStripNullableGenericReturnAnnotation(method)) + { + string fullname = method.ReturnType.Fullname; + sb.Append(fullname, 0, fullname.Length - 1); + return; + } + + sb.AppendTypeOrWrapper(method.ReturnType); + } + #pragma warning disable S107 // Methods should not have too many parameters private static void ImplementMockForInterface(StringBuilder sb, string mockRegistryName, string name, bool hasEvents, bool hasProtectedMembers, bool hasProtectedEvents, bool hasStaticMembers, bool hasStaticEvents) @@ -3913,7 +3971,8 @@ private static void AppendMethodSetupDefinition(StringBuilder sb, Class @class, : "\t\tglobal::Mockolate.Setup.IReturnMethodSetup"); } - sb.Append('<').AppendTypeOrWrapper(method.ReturnType); + sb.Append('<'); + AppendSetupReturnType(sb, method); foreach (MethodParameter parameter in method.Parameters) { sb.Append(", ").AppendTypeOrWrapper(parameter.Type); @@ -4208,7 +4267,8 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met : "\t\tglobal::Mockolate.Setup.IReturnMethodSetup"); } - sb.Append('<').AppendTypeOrWrapper(method.ReturnType); + sb.Append('<'); + AppendSetupReturnType(sb, method); foreach (MethodParameter parameter in method.Parameters) { sb.Append(", ").AppendTypeOrWrapper(parameter.Type); @@ -4299,8 +4359,8 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met string methodSetupVar = Helpers.GetUniqueLocalVariableName("methodSetup", method.Parameters); if (method.ReturnType != Type.Void) { - sb.Append("\t\t\tvar ").Append(methodSetupVar).Append(" = new global::Mockolate.Setup.ReturnMethodSetup<") - .AppendTypeOrWrapper(method.ReturnType); + sb.Append("\t\t\tvar ").Append(methodSetupVar).Append(" = new global::Mockolate.Setup.ReturnMethodSetup<"); + AppendSetupReturnType(sb, method); foreach (MethodParameter parameter in method.Parameters) {