diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt index 4b8b02f685..7cf3821cf7 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Static_Abstract_Transitive_Return_Type.verified.txt @@ -46,7 +46,7 @@ namespace TUnit.Mocks.Generated public global::IConfigProvider GetConfigProvider() { - return _engine.HandleCallWithReturn(1, "GetConfigProvider", global::System.Array.Empty(), default!); + return (global::IConfigProvider)_engine.HandleCallWithReturn(1, "GetConfigProvider", global::System.Array.Empty(), null)!; } [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -80,10 +80,16 @@ namespace TUnit.Mocks.Generated return new IMyService_GetValue_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "GetValue", matchers); } - public static global::TUnit.Mocks.MockMethodCall GetConfigProvider(this global::TUnit.Mocks.Mock mock) + /// Configure the mock setup for GetConfigProvider. + /// + /// A typed as object? because the return type + /// contains static abstract members and cannot be used as a generic type argument (CS8920). + /// Pass a value that implements the return interface to .Returns() — incorrect types will cause an at call time. + /// + public static global::TUnit.Mocks.MockMethodCall GetConfigProvider(this global::TUnit.Mocks.Mock mock) { var matchers = global::System.Array.Empty(); - return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "GetConfigProvider", matchers); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "GetConfigProvider", matchers); } } diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockBridgeBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockBridgeBuilder.cs index b278b11992..58a44fedd4 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockBridgeBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockBridgeBuilder.cs @@ -108,7 +108,14 @@ private static void GenerateStaticPropertyDim(CodeWriter writer, MockMemberModel writer.OpenBrace(); writer.AppendLine($"var __engine = {safeName}_StaticEngine.Engine;"); writer.AppendLine("if (__engine is null) return default!;"); - writer.AppendLine($"return __engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), {prop.SmartDefault});"); + if (prop.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"return ({prop.ReturnType})__engine.HandleCallWithReturn({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), null)!;"); + } + else + { + writer.AppendLine($"return __engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), {prop.SmartDefault});"); + } writer.CloseBrace(); } @@ -158,51 +165,122 @@ private static void GenerateStaticEngineDispatchBody(CodeWriter writer, MockMemb else if (method.IsVoid && method.IsAsync) { if (method.IsValueTask) + { writer.AppendLine("if (__engine is null) return default(global::System.Threading.Tasks.ValueTask);"); + } else + { writer.AppendLine("if (__engine is null) return global::System.Threading.Tasks.Task.CompletedTask;"); + } using (writer.Block("try")) { writer.AppendLine($"__engine.HandleCall({method.MemberId}, \"{method.Name}\", {argsArray});"); MockImplBuilder.EmitOutRefReadback(writer, method); if (method.IsValueTask) + { writer.AppendLine("return default(global::System.Threading.Tasks.ValueTask);"); + } else + { writer.AppendLine("return global::System.Threading.Tasks.Task.CompletedTask;"); + } } using (writer.Block("catch (global::System.Exception __ex)")) { if (method.IsValueTask) + { writer.AppendLine("return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(__ex));"); + } else + { writer.AppendLine("return global::System.Threading.Tasks.Task.FromException(__ex);"); + } + } + } + else if (method.IsAsync && method.IsReturnTypeStaticAbstractInterface) + { + // Async method whose unwrapped return type has static abstract members (CS8920). + // Use object? and cast instead. + if (method.IsValueTask) + { + writer.AppendLine($"if (__engine is null) return new global::System.Threading.Tasks.ValueTask<{method.UnwrappedReturnType}>(default!);"); + } + else + { + writer.AppendLine($"if (__engine is null) return global::System.Threading.Tasks.Task.FromResult<{method.UnwrappedReturnType}>(default!);"); + } + + using (writer.Block("try")) + { + writer.AppendLine($"var __result = ({method.UnwrappedReturnType})__engine.HandleCallWithReturn({method.MemberId}, \"{method.Name}\", {argsArray}, null)!;"); + MockImplBuilder.EmitOutRefReadback(writer, method); + if (method.IsValueTask) + { + writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask<{method.UnwrappedReturnType}>(__result);"); + } + else + { + writer.AppendLine($"return global::System.Threading.Tasks.Task.FromResult<{method.UnwrappedReturnType}>(__result);"); + } + } + using (writer.Block("catch (global::System.Exception __ex)")) + { + if (method.IsValueTask) + { + writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask<{method.UnwrappedReturnType}>(global::System.Threading.Tasks.Task.FromException<{method.UnwrappedReturnType}>(__ex));"); + } + else + { + writer.AppendLine($"return global::System.Threading.Tasks.Task.FromException<{method.UnwrappedReturnType}>(__ex);"); + } } } else if (method.IsAsync) { if (method.IsValueTask) + { writer.AppendLine($"if (__engine is null) return new global::System.Threading.Tasks.ValueTask<{method.UnwrappedReturnType}>({method.UnwrappedSmartDefault});"); + } else + { writer.AppendLine($"if (__engine is null) return global::System.Threading.Tasks.Task.FromResult<{method.UnwrappedReturnType}>({method.UnwrappedSmartDefault});"); + } using (writer.Block("try")) { writer.AppendLine($"var __result = __engine.HandleCallWithReturn<{method.UnwrappedReturnType}>({method.MemberId}, \"{method.Name}\", {argsArray}, {method.UnwrappedSmartDefault});"); MockImplBuilder.EmitOutRefReadback(writer, method); if (method.IsValueTask) + { writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask<{method.UnwrappedReturnType}>(__result);"); + } else + { writer.AppendLine($"return global::System.Threading.Tasks.Task.FromResult<{method.UnwrappedReturnType}>(__result);"); + } } using (writer.Block("catch (global::System.Exception __ex)")) { if (method.IsValueTask) + { writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask<{method.UnwrappedReturnType}>(global::System.Threading.Tasks.Task.FromException<{method.UnwrappedReturnType}>(__ex));"); + } else + { writer.AppendLine($"return global::System.Threading.Tasks.Task.FromException<{method.UnwrappedReturnType}>(__ex);"); + } } } + else if (method.IsReturnTypeStaticAbstractInterface) + { + // Return type has static abstract members — can't use as generic type argument (CS8920). + // Use object? and cast instead. + writer.AppendLine("if (__engine is null) return default!;"); + writer.AppendLine($"var __result = __engine.HandleCallWithReturn({method.MemberId}, \"{method.Name}\", {argsArray}, null);"); + MockImplBuilder.EmitOutRefReadback(writer, method); + writer.AppendLine($"return ({method.ReturnType})__result!;"); + } else { writer.AppendLine("if (__engine is null) return default!;"); diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index 7c71fadfe2..518deccb33 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -146,7 +146,9 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo writer.AppendLine("_engine = engine;"); writer.AppendLine("_wrappedInstance = wrappedInstance;"); if (model.HasStaticAbstractMembers) + { EmitStaticEngineAssignment(writer, safeName); + } } return; } @@ -160,7 +162,9 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo writer.AppendLine("_engine = engine;"); writer.AppendLine("_wrappedInstance = wrappedInstance;"); if (model.HasStaticAbstractMembers) + { EmitStaticEngineAssignment(writer, safeName); + } } } else @@ -172,7 +176,9 @@ private static void GenerateWrapConstructors(CodeWriter writer, MockTypeModel mo writer.AppendLine("_engine = engine;"); writer.AppendLine("_wrappedInstance = wrappedInstance;"); if (model.HasStaticAbstractMembers) + { EmitStaticEngineAssignment(writer, safeName); + } } } } @@ -247,9 +253,19 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me } else if (method.IsAsync) { - writer.AppendLine($"if (_engine.TryHandleCallWithReturn<{method.UnwrappedReturnType}>({method.MemberId}, \"{method.Name}\", {argsArray}, {method.UnwrappedSmartDefault}, out var __result))"); - writer.AppendLine("{"); - writer.IncreaseIndent(); + if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"if (_engine.TryHandleCallWithReturn({method.MemberId}, \"{method.Name}\", {argsArray}, null, out var __rawResult))"); + writer.AppendLine("{"); + writer.IncreaseIndent(); + writer.AppendLine($"var __result = ({method.UnwrappedReturnType})__rawResult!;"); + } + else + { + writer.AppendLine($"if (_engine.TryHandleCallWithReturn<{method.UnwrappedReturnType}>({method.MemberId}, \"{method.Name}\", {argsArray}, {method.UnwrappedSmartDefault}, out var __result))"); + writer.AppendLine("{"); + writer.IncreaseIndent(); + } EmitOutRefReadback(writer, method); if (method.IsValueTask) { @@ -281,6 +297,18 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me writer.AppendLine("}"); writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});"); } + else if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"if (_engine.TryHandleCallWithReturn({method.MemberId}, \"{method.Name}\", {argsArray}, null, out var __rawResult))"); + writer.AppendLine("{"); + writer.IncreaseIndent(); + writer.AppendLine($"var __result = ({method.ReturnType})__rawResult!;"); + EmitOutRefReadback(writer, method); + writer.AppendLine("return __result;"); + writer.DecreaseIndent(); + writer.AppendLine("}"); + writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});"); + } else { writer.AppendLine($"if (_engine.TryHandleCallWithReturn<{method.ReturnType}>({method.MemberId}, \"{method.Name}\", {argsArray}, {method.SmartDefault}, out var __result))"); @@ -326,10 +354,27 @@ private static void GenerateWrapProperty(CodeWriter writer, MockMemberModel prop writer.CloseBrace(); } } + else if (prop.IsAbstractMember && prop.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"get => ({prop.ReturnType})_engine.HandleCallWithReturn({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), null)!;"); + } else if (prop.IsAbstractMember) { writer.AppendLine($"get => _engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), {prop.SmartDefault});"); } + else if (prop.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine("get"); + writer.OpenBrace(); + writer.AppendLine($"if (_engine.TryHandleCallWithReturn({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), null, out var __rawResult))"); + writer.AppendLine("{"); + writer.IncreaseIndent(); + writer.AppendLine($"return ({prop.ReturnType})__rawResult!;"); + writer.DecreaseIndent(); + writer.AppendLine("}"); + writer.AppendLine($"return _wrappedInstance.{prop.Name};"); + writer.CloseBrace(); + } else { writer.AppendLine("get"); @@ -426,7 +471,9 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel { writer.AppendLine("_engine = engine;"); if (model.HasStaticAbstractMembers) + { EmitStaticEngineAssignment(writer, safeName); + } } return; } @@ -440,7 +487,9 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel { writer.AppendLine("_engine = engine;"); if (model.HasStaticAbstractMembers) + { EmitStaticEngineAssignment(writer, safeName); + } } } else @@ -452,7 +501,9 @@ private static void GeneratePartialConstructors(CodeWriter writer, MockTypeModel { writer.AppendLine("_engine = engine;"); if (model.HasStaticAbstractMembers) + { EmitStaticEngineAssignment(writer, safeName); + } } } } @@ -543,9 +594,19 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel else if (method.IsAsync) { // async method with return (Task/ValueTask) - writer.AppendLine($"if (_engine.TryHandleCallWithReturn<{method.UnwrappedReturnType}>({method.MemberId}, \"{method.Name}\", {argsArray}, {method.UnwrappedSmartDefault}, out var __result))"); - writer.AppendLine("{"); - writer.IncreaseIndent(); + if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"if (_engine.TryHandleCallWithReturn({method.MemberId}, \"{method.Name}\", {argsArray}, null, out var __rawResult))"); + writer.AppendLine("{"); + writer.IncreaseIndent(); + writer.AppendLine($"var __result = ({method.UnwrappedReturnType})__rawResult!;"); + } + else + { + writer.AppendLine($"if (_engine.TryHandleCallWithReturn<{method.UnwrappedReturnType}>({method.MemberId}, \"{method.Name}\", {argsArray}, {method.UnwrappedSmartDefault}, out var __result))"); + writer.AppendLine("{"); + writer.IncreaseIndent(); + } EmitOutRefReadback(writer, method); if (method.IsValueTask) { @@ -578,6 +639,18 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel writer.AppendLine("}"); writer.AppendLine($"return base.{method.Name}({argPassList});"); } + else if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"if (_engine.TryHandleCallWithReturn({method.MemberId}, \"{method.Name}\", {argsArray}, null, out var __rawResult))"); + writer.AppendLine("{"); + writer.IncreaseIndent(); + writer.AppendLine($"var __result = ({method.ReturnType})__rawResult!;"); + EmitOutRefReadback(writer, method); + writer.AppendLine("return __result;"); + writer.DecreaseIndent(); + writer.AppendLine("}"); + writer.AppendLine($"return base.{method.Name}({argPassList});"); + } else { // synchronous method with return value @@ -644,9 +717,18 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode else if (method.IsAsync) { // Async method with return value (Task or ValueTask) + var unwrappedArg = method.IsReturnTypeStaticAbstractInterface ? "object?" : method.UnwrappedReturnType; + var unwrappedDefault = method.IsReturnTypeStaticAbstractInterface ? "null" : method.UnwrappedSmartDefault; using (writer.Block("try")) { - writer.AppendLine($"var __result = _engine.HandleCallWithReturn<{method.UnwrappedReturnType}>({method.MemberId}, \"{method.Name}\", {argsArray}, {method.UnwrappedSmartDefault});"); + if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"var __result = ({method.UnwrappedReturnType})_engine.HandleCallWithReturn<{unwrappedArg}>({method.MemberId}, \"{method.Name}\", {argsArray}, {unwrappedDefault})!;"); + } + else + { + writer.AppendLine($"var __result = _engine.HandleCallWithReturn<{unwrappedArg}>({method.MemberId}, \"{method.Name}\", {argsArray}, {unwrappedDefault});"); + } EmitOutRefReadback(writer, method); if (method.IsValueTask) { @@ -686,6 +768,21 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode writer.AppendLine("return default;"); } } + else if (method.IsReturnTypeStaticAbstractInterface) + { + // Return type is an interface with static abstract members — CS8920 prevents using it + // as a generic type argument. Use object? and cast. + if (hasOutRef) + { + writer.AppendLine($"var __result = ({method.ReturnType})_engine.HandleCallWithReturn({method.MemberId}, \"{method.Name}\", {argsArray}, null)!;"); + EmitOutRefReadback(writer, method); + writer.AppendLine("return __result;"); + } + else + { + writer.AppendLine($"return ({method.ReturnType})_engine.HandleCallWithReturn({method.MemberId}, \"{method.Name}\", {argsArray}, null)!;"); + } + } else { // Synchronous method with return value — need to read back out/ref before returning @@ -718,6 +815,10 @@ private static void GenerateInterfaceProperty(CodeWriter writer, MockMemberModel writer.AppendLine("return default;"); writer.CloseBrace(); } + else if (prop.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"get => ({prop.ReturnType})_engine.HandleCallWithReturn({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), null)!;"); + } else { writer.AppendLine($"get => _engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), {prop.SmartDefault});"); @@ -772,10 +873,28 @@ private static void GeneratePartialProperty(CodeWriter writer, MockMemberModel p writer.CloseBrace(); } } + else if (prop.IsAbstractMember && prop.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"get => ({prop.ReturnType})_engine.HandleCallWithReturn({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), null)!;"); + } else if (prop.IsAbstractMember) { writer.AppendLine($"get => _engine.HandleCallWithReturn<{prop.ReturnType}>({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), {prop.SmartDefault});"); } + else if (prop.IsReturnTypeStaticAbstractInterface) + { + // Virtual property getter: try engine, fall back to base (CS8920-safe) + writer.AppendLine("get"); + writer.OpenBrace(); + writer.AppendLine($"if (_engine.TryHandleCallWithReturn({prop.MemberId}, \"get_{prop.Name}\", global::System.Array.Empty(), null, out var __rawResult))"); + writer.AppendLine("{"); + writer.IncreaseIndent(); + writer.AppendLine($"return ({prop.ReturnType})__rawResult!;"); + writer.DecreaseIndent(); + writer.AppendLine("}"); + writer.AppendLine($"return base.{prop.Name};"); + writer.CloseBrace(); + } else { // Virtual property getter: try engine, fall back to base @@ -1061,7 +1180,9 @@ private static void EmitOutRefParamAssignments(CodeWriter writer, MockMemberMode internal static string EmitArgsArrayVariable(CodeWriter writer, MockMemberModel method) { if (!method.HasRefStructParams) + { return GetArgsArrayExpression(method, false); + } writer.AppendLine("#if NET9_0_OR_GREATER"); writer.AppendLine($"var __args = {GetArgsArrayExpression(method, true)};"); diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 3713bf3188..2de65e25f4 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -52,7 +52,7 @@ public static string Build(MockTypeModel model) // Properties -- extension properties via C# 14 extension blocks // (skip ref struct properties — can't use PropertyMockCall) var memberProps = model.Properties - .Where(p => !p.IsIndexer && !p.IsRefStructReturn && (p.HasGetter || p.HasSetter)) + .Where(p => !p.IsIndexer && !p.IsRefStructReturn && !p.IsReturnTypeStaticAbstractInterface && (p.HasGetter || p.HasSetter)) .ToList(); if (memberProps.Count > 0) { @@ -121,11 +121,17 @@ private static void GenerateUnifiedSealedClass(CodeWriter writer, MockMemberMode var hasRefStructParams = method.HasRefStructParams; var allNonOutParams = method.Parameters.Where(p => p.Direction != ParameterDirection.Out).ToList(); - // Ref struct returns use the void wrapper (can't use generic type args with ref structs) + // Ref struct returns use the void wrapper (can't use ref structs as generic type args) if (method.IsVoid || method.IsRefStructReturn) { GenerateVoidUnifiedClass(writer, wrapperName, matchableParams, events, method.Parameters, hasRefStructParams, allNonOutParams, method.SpanReturnElementType, method.ReturnType); } + else if (method.IsReturnTypeStaticAbstractInterface) + { + // Static-abstract interface returns can't be used as generic type args (CS8920) + // Use object? to preserve .Returns() capability + GenerateReturnUnifiedClass(writer, wrapperName, matchableParams, "object?", events, method.Parameters, hasRefStructParams, allNonOutParams); + } else { GenerateReturnUnifiedClass(writer, wrapperName, matchableParams, setupReturnType, events, method.Parameters, hasRefStructParams, allNonOutParams); @@ -197,7 +203,9 @@ private static void GenerateReturnUnifiedClass(CodeWriter writer, string wrapper writer.AppendLine($"public {wrapperName} Raises(string eventName, object? args = null) {{ EnsureSetup().Raises(eventName, args); return this; }}"); writer.AppendLine($"/// "); if (hasOutRef) + { writer.AppendLine("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + } writer.AppendLine($"public {wrapperName} SetsOutParameter(int paramIndex, object? value) {{ EnsureSetup().SetsOutParameter(paramIndex, value); return this; }}"); writer.AppendLine($"/// "); writer.AppendLine($"public {wrapperName} TransitionsTo(string stateName) {{ EnsureSetup().TransitionsTo(stateName); return this; }}"); @@ -326,7 +334,9 @@ private static void GenerateVoidUnifiedClass(CodeWriter writer, string wrapperNa writer.AppendLine($"public {wrapperName} Raises(string eventName, object? args = null) {{ EnsureSetup().Raises(eventName, args); return this; }}"); writer.AppendLine($"/// "); if (hasOutRef) + { writer.AppendLine("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + } writer.AppendLine($"public {wrapperName} SetsOutParameter(int paramIndex, object? value) {{ EnsureSetup().SetsOutParameter(paramIndex, value); return this; }}"); writer.AppendLine($"/// "); writer.AppendLine($"public {wrapperName} TransitionsTo(string stateName) {{ EnsureSetup().TransitionsTo(stateName); return this; }}"); @@ -481,11 +491,15 @@ private static void GenerateTypedOutRefMethods(CodeWriter writer, EquatableArray { var param = allParameters[i]; if (param.Direction != ParameterDirection.Out && param.Direction != ParameterDirection.Ref) + { continue; + } // Skip non-span ref structs (can't be boxed) if (param.IsRefStruct && param.SpanElementType is null) + { continue; + } var prefix = param.Direction == ParameterDirection.Out ? "SetsOut" : "SetsRef"; var methodName = prefix + ToPascalCase(param.Name); @@ -513,7 +527,9 @@ private static string ToPascalCase(string name) private static string BuildCastArgs(List nonOutParams, List? allNonOutParams = null) { if (allNonOutParams is null) + { return string.Join(", ", nonOutParams.Select((p, i) => $"({p.FullyQualifiedType})args[{i}]!")); + } var indexMap = allNonOutParams.Select((p, i) => (p, i)).ToDictionary(x => x.p, x => x.i); return string.Join(", ", nonOutParams.Select(p => $"({p.FullyQualifiedType})args[{indexMap[p]}]!")); @@ -550,11 +566,21 @@ private static (bool UseTypedWrapper, string ReturnType, string SetupReturnType) string returnType; if (useTypedWrapper) + { returnType = GetWrapperName(safeName, method); + } else if (method.IsVoid || method.IsRefStructReturn) + { returnType = "global::TUnit.Mocks.VoidMockMethodCall"; + } + else if (method.IsReturnTypeStaticAbstractInterface) + { + returnType = "global::TUnit.Mocks.MockMethodCall"; + } else + { returnType = $"global::TUnit.Mocks.MockMethodCall<{setupReturnType}>"; + } return (useTypedWrapper, returnType, setupReturnType); } @@ -572,6 +598,16 @@ private static void EmitMemberMethodBody(CodeWriter writer, MockMemberModel meth var extensionParam = $"this global::TUnit.Mocks.Mock<{mockableType}> mock"; var fullParamList = string.IsNullOrEmpty(paramList) ? extensionParam : $"{extensionParam}, {paramList}"; + if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"/// Configure the mock setup for {method.Name}."); + writer.AppendLine($"/// "); + writer.AppendLine($"/// A typed as object? because the return type"); + writer.AppendLine($"/// contains static abstract members and cannot be used as a generic type argument (CS8920)."); + writer.AppendLine($"/// Pass a value that implements the return interface to .Returns() — incorrect types will cause an at call time."); + writer.AppendLine($"/// "); + } + using (writer.Block($"public static {returnType} {safeMemberName}{typeParams}({fullParamList}){constraints}")) { // Build matchers array @@ -598,6 +634,10 @@ private static void EmitMemberMethodBody(CodeWriter writer, MockMemberModel meth { writer.AppendLine($"return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); } + else if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + } else { writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); @@ -642,7 +682,9 @@ private static void EmitSingleFuncOverload(CodeWriter writer, MockMemberModel me for (int bit = 0; bit < eligibleIndices.Count; bit++) { if ((funcMask & (1 << bit)) != 0) + { funcIndices.Add(eligibleIndices[bit]); + } } var (useTypedWrapper, returnType, setupReturnType) = GetReturnTypeInfo(method, model, safeName); @@ -661,7 +703,9 @@ private static void EmitSingleFuncOverload(CodeWriter writer, MockMemberModel me else if (p.IsRefStruct) { if (includeRefStructArgs) + { paramParts.Add($"global::TUnit.Mocks.Arguments.RefStructArg<{p.FullyQualifiedType}> {p.Name}"); + } } else { @@ -678,6 +722,16 @@ private static void EmitSingleFuncOverload(CodeWriter writer, MockMemberModel me var extensionParam = $"this global::TUnit.Mocks.Mock<{mockableType}> mock"; var fullParamList = string.IsNullOrEmpty(paramList) ? extensionParam : $"{extensionParam}, {paramList}"; + if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"/// Configure the mock setup for {method.Name}."); + writer.AppendLine($"/// "); + writer.AppendLine($"/// A typed as object? because the return type"); + writer.AppendLine($"/// contains static abstract members and cannot be used as a generic type argument (CS8920)."); + writer.AppendLine($"/// Pass a value that implements the return interface to .Returns() — incorrect types will cause an at call time."); + writer.AppendLine($"/// "); + } + using (writer.Block($"public static {returnType} {safeMemberName}{typeParams}({fullParamList}){constraints}")) { // Convert Func params to Arg via implicit conversion @@ -719,6 +773,10 @@ private static void EmitSingleFuncOverload(CodeWriter writer, MockMemberModel me { writer.AppendLine($"return new global::TUnit.Mocks.VoidMockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); } + else if (method.IsReturnTypeStaticAbstractInterface) + { + writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); + } else { writer.AppendLine($"return new global::TUnit.Mocks.MockMethodCall<{setupReturnType}>(global::TUnit.Mocks.Mock.GetEngine(mock), {method.MemberId}, \"{method.Name}\", matchers);"); @@ -799,7 +857,9 @@ private static string GetArgParameterList(MockMemberModel method, bool includeRe if (p.IsRefStruct) { if (includeRefStructArgs) + { parts.Add($"global::TUnit.Mocks.Arguments.RefStructArg<{p.FullyQualifiedType}> {p.Name}"); + } } else { diff --git a/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs b/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs index dbd2f74303..7dc063b15d 100644 --- a/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs +++ b/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs @@ -298,6 +298,11 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m var (unwrappedType, isVoidAsync) = returnType.GetUnwrappedReturnType(); var isVoid = method.ReturnsVoid || isVoidAsync; + // Check if the effective return type (unwrapped for async) is an interface with + // static abstract members. Such types cannot be used as generic type arguments (CS8920). + var effectiveReturnTypeSymbol = returnType.GetAsyncInnerTypeSymbol() ?? returnType; + var returnTypeHasStaticAbstract = !isVoid && IsInterfaceWithStaticAbstractMembers(effectiveReturnTypeSymbol); + return new MockMemberModel { Name = method.Name, @@ -339,6 +344,7 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m IsProtected = method.DeclaredAccessibility == Accessibility.Protected || method.DeclaredAccessibility == Accessibility.ProtectedOrInternal, IsRefStructReturn = returnType.IsRefLikeType, + IsReturnTypeStaticAbstractInterface = returnTypeHasStaticAbstract, SpanReturnElementType = returnType.IsRefLikeType ? GetSpanElementType(returnType) : null }; } @@ -390,6 +396,7 @@ private static MockMemberModel CreatePropertyModel(IPropertySymbol property, ref IsProtected = property.DeclaredAccessibility == Accessibility.Protected || property.DeclaredAccessibility == Accessibility.ProtectedOrInternal, IsRefStructReturn = property.Type.IsRefLikeType, + IsReturnTypeStaticAbstractInterface = IsInterfaceWithStaticAbstractMembers(property.Type), SpanReturnElementType = property.Type.IsRefLikeType ? GetSpanElementType(property.Type) : null }; } @@ -525,7 +532,9 @@ private static MockEventModel CreateEventModel(IEventSymbol evt, string? explici private static bool IsEventHandlerType(ITypeSymbol type) { if (type is not INamedTypeSymbol namedType) + { return false; + } // Check if the type is System.EventHandler or System.EventHandler var fullName = namedType.ConstructedFrom.ToDisplayString(); @@ -581,7 +590,9 @@ private static string EscapeIdentifier(string name) => private static string? GetSpanElementType(ITypeSymbol type) { if (type is not INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1 } namedType) + { return null; + } var constructed = namedType.ConstructedFrom; var ns = constructed.ContainingNamespace?.ToDisplayString(); @@ -595,6 +606,25 @@ private static string EscapeIdentifier(string name) => return null; } + /// + /// Returns true when the given type symbol is an interface that contains static abstract members + /// (directly or via inherited interfaces) without a most specific implementation. + /// Such types cannot be used as generic type arguments (CS8920). + /// + private static bool IsInterfaceWithStaticAbstractMembers(ITypeSymbol type) + { + if (type is not INamedTypeSymbol { TypeKind: TypeKind.Interface } namedType) + { + return false; + } + + // Check both direct members and inherited interface members. + // AllInterfaces does NOT include the type itself, so we check both. + return namedType.GetMembers() + .Concat(namedType.AllInterfaces.SelectMany(i => i.GetMembers())) + .Any(m => m.IsStatic && m.IsAbstract); + } + /// /// Collects a static abstract interface member as a full MockMemberModel with IsStaticAbstract=true. /// Uses the existing CreateMethodModel/CreatePropertyModel to generate models with MemberIds, diff --git a/TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs b/TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs index a2804c4530..11ba77e751 100644 --- a/TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs +++ b/TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs @@ -33,6 +33,13 @@ internal sealed record MockMemberModel : IEquatable public bool IsRefStructReturn { get; init; } public bool IsStaticAbstract { get; init; } + /// + /// True when the (unwrapped) return type is an interface that has static abstract members + /// without a most specific implementation. Such types cannot be used as generic type arguments + /// (CS8920), so the generator must fall back to the void wrapper path. + /// + public bool IsReturnTypeStaticAbstractInterface { get; init; } + /// /// For methods returning ReadOnlySpan<T> or Span<T>, the fully qualified element type. /// Null for non-span return types. Used to support configurable span return values via array conversion. @@ -72,6 +79,7 @@ public bool Equals(MockMemberModel? other) && IsProtected == other.IsProtected && IsRefStructReturn == other.IsRefStructReturn && IsStaticAbstract == other.IsStaticAbstract + && IsReturnTypeStaticAbstractInterface == other.IsReturnTypeStaticAbstractInterface && SpanReturnElementType == other.SpanReturnElementType; } @@ -85,6 +93,7 @@ public override int GetHashCode() hash = hash * 31 + ReturnType.GetHashCode(); hash = hash * 31 + Parameters.GetHashCode(); hash = hash * 31 + IsStaticAbstract.GetHashCode(); + hash = hash * 31 + IsReturnTypeStaticAbstractInterface.GetHashCode(); return hash; } } diff --git a/TUnit.Mocks.Tests/StaticAbstractMemberTests.cs b/TUnit.Mocks.Tests/StaticAbstractMemberTests.cs index 1bb5f5458f..fc87d2ac4a 100644 --- a/TUnit.Mocks.Tests/StaticAbstractMemberTests.cs +++ b/TUnit.Mocks.Tests/StaticAbstractMemberTests.cs @@ -1,4 +1,5 @@ #if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; using TUnit.Mocks; using TUnit.Mocks.Generated; @@ -14,6 +15,17 @@ public class ClientConfig public string Region { get; set; } = "us-east-1"; } +public class AWSCredentials +{ + public string AccessKey { get; set; } = ""; + public string AuthSignature { get; set; } = ""; +} + +public interface IClientConfig +{ + string Region { get; } +} + public interface IServiceConfig { static abstract ClientConfig CreateDefaultConfig(); @@ -22,8 +34,24 @@ public interface IServiceConfig public interface IAmazonService : IServiceConfig { + /// A readonly view of the configuration for the service client. + IClientConfig Config { get; } + string GetEndpoint(); void Initialize(string region); + + /// Factory method for creating the service client config object. + static abstract ClientConfig CreateDefaultClientConfig(); + + /// + /// Factory method for creating the default implementation of the AWS service interface. + /// Returns , which itself has static abstract members — + /// this is the CS8920 transitive scenario our fix addresses. + /// + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026")] + static abstract IAmazonService CreateDefaultServiceClient( + AWSCredentials awsCredentials, + ClientConfig clientConfig); } /// @@ -133,6 +161,93 @@ public async Task Instance_And_Static_Members_Coexist() await Assert.That(config!.Region).IsEqualTo("ap-southeast-1"); } + // --- CreateDefaultClientConfig (static abstract, returns concrete ClientConfig) --- + + [Test] + public async Task Static_Abstract_CreateDefaultClientConfig_Returns_Configured_Value() + { + var mock = Mock.Of(); + var expected = new ClientConfig { Region = "sa-east-1" }; + mock.CreateDefaultClientConfig().Returns(expected); + + var result = CallCreateDefaultClientConfig(); + + await Assert.That(result).IsSameReferenceAs(expected); + } + + [Test] + public async Task Static_Abstract_CreateDefaultClientConfig_Verification() + { + var mock = Mock.Of(); + mock.CreateDefaultClientConfig().Returns(new ClientConfig()); + + CallCreateDefaultClientConfig(); + CallCreateDefaultClientConfig(); + + mock.CreateDefaultClientConfig().WasCalled(Times.Exactly(2)); + } + + // --- CreateDefaultServiceClient (static abstract, returns IAmazonService — the CS8920 transitive scenario) --- + + [Test] + public async Task Static_Abstract_CreateDefaultServiceClient_Returns_Null_By_Default() + { + // No setup — the generator uses HandleCallWithReturn + cast, + // so the default null value is cast to IAmazonService and returned. + var mock = Mock.Of(); + + var result = CallCreateDefaultServiceClient( + new AWSCredentials(), new ClientConfig()); + + // Cast to object? — using IAmazonService directly as a type argument triggers CS8920. + await Assert.That((object?)result).IsNull(); + } + + [Test] + public async Task Static_Abstract_CreateDefaultServiceClient_Returns_Configured_Value() + { + // Arrange — .Returns() works via MockMethodCall (not VoidMockMethodCall) + // because the return type (IAmazonService) has static abstract members (CS8920). + var mock = Mock.Of(); + var expectedService = mock.Object; + mock.CreateDefaultServiceClient(Arg.Any(), Arg.Any()) + .Returns(expectedService); + + // Act + var result = CallCreateDefaultServiceClient( + new AWSCredentials(), new ClientConfig()); + + // Assert — the configured value is returned through the object? → IAmazonService cast + await Assert.That((object?)result).IsSameReferenceAs(expectedService); + } + + [Test] + public async Task Static_Abstract_CreateDefaultServiceClient_Verification() + { + var mock = Mock.Of(); + var creds = new AWSCredentials { AccessKey = "AKID", AuthSignature = "test-sig" }; + var config = new ClientConfig { Region = "us-west-2" }; + + CallCreateDefaultServiceClient(creds, config); + + // MockMethodCall supports verification even though the return type + // (IAmazonService) cannot be used as a generic type argument (CS8920). + mock.CreateDefaultServiceClient(Arg.Any(), Arg.Any()).WasCalled(); + } + + [Test] + public async Task Static_Abstract_CreateDefaultServiceClient_Throws_Configured_Exception() + { + var mock = Mock.Of(); + mock.CreateDefaultServiceClient(Arg.Any(), Arg.Any()) + .Throws(new InvalidOperationException("service unavailable")); + + await Assert.That(() => CallCreateDefaultServiceClient( + new AWSCredentials(), new ClientConfig())) + .ThrowsExactly() + .WithMessage("service unavailable"); + } + /// /// Calls the static abstract method through a constrained generic, which is the only /// way to invoke static abstract members in C#. @@ -145,5 +260,12 @@ public async Task Instance_And_Static_Members_Coexist() private static void SetStaticAbstractProperty(string value) where T : IServiceConfig => T.ServiceId = value; + + private static ClientConfig? CallCreateDefaultClientConfig() where T : IAmazonService + => T.CreateDefaultClientConfig(); + + private static IAmazonService? CallCreateDefaultServiceClient(AWSCredentials creds, ClientConfig config) + where T : IAmazonService + => T.CreateDefaultServiceClient(creds, config); } #endif