diff --git a/Source/Mockolate.SourceGenerators/Entities/Method.cs b/Source/Mockolate.SourceGenerators/Entities/Method.cs index 0312aee7..b6c6d761 100644 --- a/Source/Mockolate.SourceGenerators/Entities/Method.cs +++ b/Source/Mockolate.SourceGenerators/Entities/Method.cs @@ -87,6 +87,81 @@ internal string GetUniqueNameString() return $"\"{ContainingType}.{Name}\""; } + /// + /// A method has an unsupported allows ref struct type parameter when one of its + /// own generic parameters declares the anti-constraint and is referenced in the + /// return type or any parameter type. The standard setup pipeline parameterizes + /// ReturnMethodSetup<T> / IReturnMethodSetup<T> on T, but + /// those runtime types do not carry allows ref struct, so the generated source + /// would fail with CS9244. Methods that match this predicate get a NotSupportedException + /// stub instead — see the carve-out for unsupported ref-struct shapes for the same shape. + /// + public bool HasUnsupportedAllowsRefStructTypeParameter + { + get + { + if (GenericParameters is null || GenericParameters.Value.Count == 0) + { + return false; + } + + GenericParameter[] refStructParameters = GenericParameters.Value + .Where(g => g.AllowsRefStruct).ToArray(); + if (refStructParameters.Length == 0) + { + return false; + } + + if (ReturnType != Type.Void && ContainsAnyTypeParameter(ReturnType.Fullname, refStructParameters)) + { + return true; + } + + foreach (MethodParameter parameter in Parameters) + { + if (ContainsAnyTypeParameter(parameter.Type.Fullname, refStructParameters)) + { + return true; + } + } + + return false; + } + } + + private static bool ContainsAnyTypeParameter(string text, GenericParameter[] genericParameters) + { + foreach (GenericParameter gp in genericParameters) + { + if (ContainsAsToken(text, gp.Name)) + { + return true; + } + } + + return false; + } + + private static bool ContainsAsToken(string text, string name) + { + int idx = 0; + while ((idx = text.IndexOf(name, idx, StringComparison.Ordinal)) >= 0) + { + bool startBoundary = idx == 0 || !IsIdentifierChar(text[idx - 1]); + bool endBoundary = idx + name.Length == text.Length || !IsIdentifierChar(text[idx + name.Length]); + if (startBoundary && endBoundary) + { + return true; + } + + idx++; + } + + return false; + } + + private static bool IsIdentifierChar(char c) => char.IsLetterOrDigit(c) || c == '_'; + public bool IsToString() => Name == "ToString" && Parameters.Count == 0; diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index b7b55a8d..0b7f6585 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -3002,6 +3002,20 @@ private static void AppendMockSubject_ImplementClass_AddMethod(StringBuilder sb, sb.AppendLine(); sb.AppendLine("\t\t{"); + // Methods that use a generic type parameter declaring `allows ref struct` in their + // signature cannot route through the regular setup pipeline: ReturnMethodSetup / + // IReturnMethodSetup do not declare the same anti-constraint, so referencing them + // with such a T fails with CS9244. Mirroring the carve-out for unsupported ref-struct + // shapes, the body throws NotSupportedException so the rest of the type still compiles. + if (method.HasUnsupportedAllowsRefStructTypeParameter) + { + sb.Append( + "\t\t\tthrow new global::System.NotSupportedException(\"Mockolate: methods with a generic type parameter declaring 'allows ref struct' are not supported. Method '") + .Append(method.ContainingType).Append('.').Append(method.Name).Append("'.\");").AppendLine(); + sb.AppendLine("\t\t}"); + return; + } + // Methods with at least one ref-struct parameter (outside the Span/ReadOnlySpan wrapper // carve-out) route through the ref-struct setup pipeline. The ref-struct value cannot // be captured in a closure, so we emit a synchronous, stack-bound match/invoke loop. @@ -3902,6 +3916,14 @@ private static void AppendMethodSetupDefinition(StringBuilder sb, Class @class, bool useParameters, string? methodNameOverride = null, bool[]? valueFlags = null, bool hasOverloadResolutionPriority = false) { + // Methods using a generic type parameter that declares `allows ref struct` cannot expose + // a setup surface: IReturnMethodSetup / IVoidMethodSetup do not carry the same + // anti-constraint. The override body throws NotSupportedException, so no setup is needed. + if (method.HasUnsupportedAllowsRefStructTypeParameter) + { + return; + } + // Ref-struct pipeline: emit only the narrow IRefStruct*Setup declaration. We skip the // value-flag overloads entirely because an explicit ref-struct value cannot be captured // via `It.Is(value)` (the static value would need to live in a class field). We also @@ -4236,6 +4258,14 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met string? methodNameOverride = null, bool[]? valueFlags = null, string? scopeExpression = null) { + // Setup-side carve-out: methods using a generic type parameter that declares + // `allows ref struct` have no setup interface declaration (see + // AppendMethodSetupDefinition), so no explicit implementation is emitted either. + if (method.HasUnsupportedAllowsRefStructTypeParameter) + { + return; + } + if (method.Parameters.Any(p => p.NeedsRefStructPipeline())) { // Emit exactly once: skip the useParameters=true variant (IParameters collection diff --git a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs index 73e8e8ac..9d89b0e2 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/MockTests.ClassTests.MethodTests.cs @@ -179,6 +179,35 @@ public bool MyMethod1(int index) """).IgnoringNewlineStyle(); } + [Fact] + public async Task GenericMethodWithAllowsRefStruct_ShouldCompile() + { + GeneratorResult result = Generator + .Run(""" + using Mockolate; + + namespace MyCode; + + public class Program + { + public static void Main(string[] args) + { + _ = IMyService.CreateMock(); + } + } + + public interface IMyService + { + T G8() where T : allows ref struct; + } + """); + + await That(result.Diagnostics).IsEmpty(); + await That(result.Sources).ContainsKey("Mock.IMyService.g.cs"); + await That(result.Sources["Mock.IMyService.g.cs"]) + .Contains("throw new global::System.NotSupportedException(\"Mockolate: methods with a generic type parameter declaring 'allows ref struct' are not supported. Method 'global::MyCode.IMyService.G8'.\");"); + } + [Fact] public async Task InterfaceMethodWithParameterNamedMethodExecution_ShouldGenerateUniqueLocalVariableName() {