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
77 changes: 66 additions & 11 deletions TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,15 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
writer.AppendLine($"if (_engine.TryHandleCall({method.MemberId}, \"{method.Name}\", {argsArray}))");
writer.AppendLine("{");
writer.IncreaseIndent();
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
if (method.SpanReturnElementType is not null)
{
EmitSpanReturnReadback(writer, method);
}
else
{
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
}
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});");
Expand Down Expand Up @@ -520,8 +527,15 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
writer.AppendLine($"if (_engine.TryHandleCall({method.MemberId}, \"{method.Name}\", {argsArray}))");
writer.AppendLine("{");
writer.IncreaseIndent();
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
if (method.SpanReturnElementType is not null)
{
EmitSpanReturnReadback(writer, method);
}
else
{
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
}
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{method.Name}({argPassList});");
Expand Down Expand Up @@ -621,10 +635,18 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode
{
// Synchronous method returning a ref struct — can't use HandleCallWithReturn<T> because
// ref structs can't be generic type arguments. Use void dispatch for call tracking,
// callbacks, and throws. Return default (e.g. ReadOnlySpan<byte>.Empty).
// callbacks, and throws.
writer.AppendLine($"_engine.HandleCall({method.MemberId}, \"{method.Name}\", {argsArray});");
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
if (method.SpanReturnElementType is not null)
{
// Span return: read back out/ref params AND extract return value from OutRefContext index -1
EmitSpanReturnReadback(writer, method);
}
else
{
EmitOutRefReadback(writer, method);
writer.AppendLine("return default;");
}
}
else
{
Expand Down Expand Up @@ -943,11 +965,44 @@ private static void EmitOutRefReadback(CodeWriter writer, MockMemberModel method
writer.AppendLine("var __outRef = global::TUnit.Mocks.Setup.OutRefContext.Consume();");
using (writer.Block("if (__outRef is not null)"))
{
for (int i = 0; i < method.Parameters.Length; i++)
EmitOutRefParamAssignments(writer, method);
}
}

/// <summary>
/// For ref struct return methods with span support: emits code to consume OutRefContext,
/// read back out/ref params, extract span return value, and return.
/// Always ends with "return default;" as fallback.
/// </summary>
private static void EmitSpanReturnReadback(CodeWriter writer, MockMemberModel method)
{
writer.AppendLine("var __outRef = global::TUnit.Mocks.Setup.OutRefContext.Consume();");
using (writer.Block("if (__outRef is not null)"))
{
EmitOutRefParamAssignments(writer, method);
writer.AppendLine($"if (__outRef.TryGetValue(global::TUnit.Mocks.Setup.OutRefContext.SpanReturnValueIndex, out var __spanRet)) return new {method.ReturnType}(({method.SpanReturnElementType}[])__spanRet!);");
}
writer.AppendLine("return default;");
}

/// <summary>
/// Emits individual out/ref parameter assignments from the __outRef dictionary.
/// Shared by <see cref="EmitOutRefReadback"/> and <see cref="EmitSpanReturnReadback"/>.
/// </summary>
private static void EmitOutRefParamAssignments(CodeWriter writer, MockMemberModel method)
{
for (int i = 0; i < method.Parameters.Length; i++)
{
var p = method.Parameters[i];
if (p.IsRefStruct && p.SpanElementType is null) continue; // non-span ref structs can't be cast from object
if (p.Direction == ParameterDirection.Out || p.Direction == ParameterDirection.Ref)
{
var p = method.Parameters[i];
if (p.IsRefStruct) continue; // ref structs can't be cast from object
if (p.Direction == ParameterDirection.Out || p.Direction == ParameterDirection.Ref)
if (p.SpanElementType is not null)
{
// Span types: reconstruct from stored array
writer.AppendLine($"if (__outRef.TryGetValue({i}, out var __v{i})) {p.Name} = new {p.FullyQualifiedType}(({p.SpanElementType}[])__v{i}!);");
}
else
{
writer.AppendLine($"if (__outRef.TryGetValue({i}, out var __v{i})) {p.Name} = ({p.FullyQualifiedType})__v{i}!;");
}
Expand Down
35 changes: 30 additions & 5 deletions TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,13 @@ private static bool ShouldGenerateTypedWrapper(MockMemberModel method, bool hasE
var matchableParams = method.Parameters.Where(p => p.Direction != ParameterDirection.Out && !p.IsRefStruct).ToList();
if (matchableParams.Count == 0)
{
// Include span-type ref struct out/ref params (supported via array conversion)
var hasOutRefParams = method.Parameters.Any(p =>
!p.IsRefStruct && (p.Direction == ParameterDirection.Out || p.Direction == ParameterDirection.Ref));
return hasEvents || hasOutRefParams;
(!p.IsRefStruct || p.SpanElementType is not null) &&
(p.Direction == ParameterDirection.Out || p.Direction == ParameterDirection.Ref));
// Span return types need a typed wrapper for the generated Returns(SpanType) method
var hasSpanReturn = method.SpanReturnElementType is not null;
return hasEvents || hasOutRefParams || hasSpanReturn;
}
return matchableParams.Count <= MaxTypedParams;
}
Expand All @@ -116,7 +120,7 @@ private static void GenerateUnifiedSealedClass(CodeWriter writer, MockMemberMode
// Ref struct returns use the void wrapper (can't use generic type args with ref structs)
if (method.IsVoid || method.IsRefStructReturn)
{
GenerateVoidUnifiedClass(writer, wrapperName, matchableParams, events, method.Parameters, hasRefStructParams, allNonOutParams);
GenerateVoidUnifiedClass(writer, wrapperName, matchableParams, events, method.Parameters, hasRefStructParams, allNonOutParams, method.SpanReturnElementType, method.ReturnType);
}
else
{
Expand Down Expand Up @@ -260,7 +264,8 @@ private static void GenerateReturnUnifiedClass(CodeWriter writer, string wrapper

private static void GenerateVoidUnifiedClass(CodeWriter writer, string wrapperName,
List<MockParameterModel> nonOutParams, EquatableArray<MockEventModel> events,
EquatableArray<MockParameterModel> allParameters, bool hasRefStructParams, List<MockParameterModel> allNonOutParams)
EquatableArray<MockParameterModel> allParameters, bool hasRefStructParams, List<MockParameterModel> allNonOutParams,
string? spanReturnElementType = null, string? spanReturnType = null)
{
var builderType = "global::TUnit.Mocks.Setup.VoidMethodSetupBuilder";
var hasOutRef = allParameters.Any(p => p.Direction == ParameterDirection.Out || p.Direction == ParameterDirection.Ref);
Expand Down Expand Up @@ -324,6 +329,14 @@ private static void GenerateVoidUnifiedClass(CodeWriter writer, string wrapperNa
writer.AppendLine($"/// <inheritdoc />");
writer.AppendLine($"public {wrapperName} Then() {{ EnsureSetup().Then(); return this; }}");

// Span return support: generate Returns(SpanType) that stores via SetsOutParameter(-1, ...)
if (spanReturnElementType is not null && spanReturnType is not null)
{
writer.AppendLine();
writer.AppendLine($"/// <summary>Configure the return value for this span-returning method.</summary>");
writer.AppendLine($"public {wrapperName} Returns({spanReturnType} value) {{ EnsureSetup().SetsOutParameter(global::TUnit.Mocks.Setup.OutRefContext.SpanReturnValueIndex, value.ToArray()); return this; }}");
}

// Typed parameter overloads (only for methods with typed params)
if (nonOutParams.Count >= 1)
{
Expand Down Expand Up @@ -466,12 +479,24 @@ private static void GenerateTypedOutRefMethods(CodeWriter writer, EquatableArray
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);
var dirLabel = param.Direction == ParameterDirection.Out ? "out" : "ref";

writer.AppendLine($"/// <summary>Sets the '{param.Name}' {dirLabel} parameter to the specified value when this setup matches.</summary>");
writer.AppendLine($"public {wrapperName} {methodName}({param.FullyQualifiedType} {param.Name}) {{ EnsureSetup().SetsOutParameter({i}, {param.Name}); return this; }}");
if (param.SpanElementType is not null)
{
// Span types: convert to array for storage, reconstruct at invocation time
writer.AppendLine($"public {wrapperName} {methodName}({param.FullyQualifiedType} {param.Name}) {{ EnsureSetup().SetsOutParameter({i}, {param.Name}.ToArray()); return this; }}");
}
else
{
writer.AppendLine($"public {wrapperName} {methodName}({param.FullyQualifiedType} {param.Name}) {{ EnsureSetup().SetsOutParameter({i}, {param.Name}); return this; }}");
}
}
}

Expand Down
30 changes: 27 additions & 3 deletions TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m
HasDefaultValue = p.HasExplicitDefaultValue,
DefaultValueExpression = p.HasExplicitDefaultValue ? FormatDefaultValue(p) : null,
IsValueType = p.Type.IsValueType,
IsRefStruct = p.Type.IsRefLikeType
IsRefStruct = p.Type.IsRefLikeType,
SpanElementType = GetSpanElementType(p.Type)
}).ToImmutableArray()
),
TypeParameters = new EquatableArray<MockTypeParameterModel>(
Expand All @@ -319,7 +320,8 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m
IsVirtualMember = method.IsVirtual || method.IsOverride,
IsProtected = method.DeclaredAccessibility == Accessibility.Protected
|| method.DeclaredAccessibility == Accessibility.ProtectedOrInternal,
IsRefStructReturn = returnType.IsRefLikeType
IsRefStructReturn = returnType.IsRefLikeType,
SpanReturnElementType = returnType.IsRefLikeType ? GetSpanElementType(returnType) : null
};
}

Expand Down Expand Up @@ -369,7 +371,8 @@ private static MockMemberModel CreatePropertyModel(IPropertySymbol property, ref
IsVirtualMember = property.IsVirtual || property.IsOverride,
IsProtected = property.DeclaredAccessibility == Accessibility.Protected
|| property.DeclaredAccessibility == Accessibility.ProtectedOrInternal,
IsRefStructReturn = property.Type.IsRefLikeType
IsRefStructReturn = property.Type.IsRefLikeType,
SpanReturnElementType = property.Type.IsRefLikeType ? GetSpanElementType(property.Type) : null
};
}

Expand Down Expand Up @@ -545,4 +548,25 @@ private static string GetMethodKey(IMethodSymbol method)
if (value is char c) return $"'{c}'";
return value.ToString();
}

/// <summary>
/// For ReadOnlySpan&lt;T&gt; or Span&lt;T&gt; types, returns the fully qualified element type.
/// Returns null for all other types.
/// </summary>
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();
var name = constructed.MetadataName;

if (ns == "System" && name is "ReadOnlySpan`1" or "Span`1")
{
return namedType.TypeArguments[0].GetFullyQualifiedName();
}

return null;
}
}
9 changes: 8 additions & 1 deletion TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ internal sealed record MockMemberModel : IEquatable<MockMemberModel>
public bool IsProtected { get; init; }
public bool IsRefStructReturn { get; init; }

/// <summary>
/// For methods returning ReadOnlySpan&lt;T&gt; or Span&lt;T&gt;, the fully qualified element type.
/// Null for non-span return types. Used to support configurable span return values via array conversion.
/// </summary>
public string? SpanReturnElementType { get; init; }

/// <summary>
/// Returns true if the method has any non-out ref struct parameters.
/// Computed from <see cref="Parameters"/> — does not participate in equality.
Expand Down Expand Up @@ -63,7 +69,8 @@ public bool Equals(MockMemberModel? other)
&& IsAbstractMember == other.IsAbstractMember
&& IsVirtualMember == other.IsVirtualMember
&& IsProtected == other.IsProtected
&& IsRefStructReturn == other.IsRefStructReturn;
&& IsRefStructReturn == other.IsRefStructReturn
&& SpanReturnElementType == other.SpanReturnElementType;
}

public override int GetHashCode()
Expand Down
10 changes: 9 additions & 1 deletion TUnit.Mocks.SourceGenerator/Models/MockParameterModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ internal sealed record MockParameterModel : IEquatable<MockParameterModel>
public bool IsValueType { get; init; }
public bool IsRefStruct { get; init; }

/// <summary>
/// For ReadOnlySpan&lt;T&gt; or Span&lt;T&gt; parameters, the fully qualified element type (e.g. "byte").
/// Null for non-span parameters. Used to support out/ref span parameters via array conversion.
/// </summary>
public string? SpanElementType { get; init; }

public bool Equals(MockParameterModel? other)
{
if (other is null) return false;
Expand All @@ -21,7 +27,8 @@ public bool Equals(MockParameterModel? other)
&& FullyQualifiedType == other.FullyQualifiedType
&& Direction == other.Direction
&& IsValueType == other.IsValueType
&& IsRefStruct == other.IsRefStruct;
&& IsRefStruct == other.IsRefStruct
&& SpanElementType == other.SpanElementType;
}

public override int GetHashCode()
Expand All @@ -34,6 +41,7 @@ public override int GetHashCode()
hash = hash * 31 + (int)Direction;
hash = hash * 31 + IsValueType.GetHashCode();
hash = hash * 31 + IsRefStruct.GetHashCode();
hash = hash * 31 + (SpanElementType?.GetHashCode() ?? 0);
return hash;
}
}
Expand Down
Loading
Loading