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
75 changes: 75 additions & 0 deletions Source/Mockolate.SourceGenerators/Entities/Method.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,81 @@ internal string GetUniqueNameString()
return $"\"{ContainingType}.{Name}\"";
}

/// <summary>
/// A method has an unsupported <c>allows ref struct</c> type parameter when one of its
/// own generic parameters declares the anti-constraint <i>and</i> is referenced in the
/// return type or any parameter type. The standard setup pipeline parameterizes
/// <c>ReturnMethodSetup&lt;T&gt;</c> / <c>IReturnMethodSetup&lt;T&gt;</c> on <c>T</c>, but
/// those runtime types do not carry <c>allows ref struct</c>, 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.
/// </summary>
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;

Expand Down
30 changes: 30 additions & 0 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> /
// IReturnMethodSetup<T> 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.
Expand Down Expand Up @@ -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<T> / IVoidMethodSetup<T> 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<T>(value)` (the static value would need to live in a class field). We also
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,35 @@ public bool MyMethod1<T, U>(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<T>() 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<T>'.\");");
}

[Fact]
public async Task InterfaceMethodWithParameterNamedMethodExecution_ShouldGenerateUniqueLocalVariableName()
{
Expand Down