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
39 changes: 36 additions & 3 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5251,6 +5251,16 @@ private static void ImplementRaiseInterface(StringBuilder sb, Class @class, stri
.Append(FormatParametersWithTypeAndName(@event.Delegate.Parameters))
.Append(")").AppendLine();
sb.AppendLine("\t\t{");
// Pre-assign declared `out` parameters: when no subscriber exists the conditional
// Invoke is skipped, so the compiler-required definite assignment must come from us.
foreach (MethodParameter p in @event.Delegate.Parameters)
{
if (p.RefKind == RefKind.Out)
{
sb.Append("\t\t\t").Append(p.Name).Append(" = default!;").AppendLine();
}
}

sb.Append("\t\t\t").Append(backingFieldAccess).Append("?.Invoke(");
if (@event.Delegate.Parameters.Count > 0)
{
Expand Down Expand Up @@ -5279,13 +5289,36 @@ private static void ImplementRaiseInterface(StringBuilder sb, Class @class, stri
sb.AppendLine("\t\t{");
sb.Append("\t\t\tglobal::Mockolate.MockBehavior mockBehavior = ").Append(mockRegistry).Append(".Behavior;")
.AppendLine();
// ref/out delegate parameters can't accept arbitrary expressions — bind each generated
// default to a local so it has an addressable storage location for the Invoke call.
bool hasByRef = @event.Delegate.Parameters.Any(p
=> p.RefKind == RefKind.Ref || p.RefKind == RefKind.Out ||
p.RefKind == RefKind.In || p.RefKind == RefKind.RefReadOnlyParameter);

List<string> argNames = new();
int idx = 0;
foreach (MethodParameter p in @event.Delegate.Parameters)
{
idx++;
string defaultExpr = $"mockBehavior.DefaultValue.Generate(default({p.Type.Fullname.TrimEnd('?')}))";
if (hasByRef)
{
string local = $"__arg{idx}";
sb.Append("\t\t\t").Append(p.Type.Fullname).Append(' ').Append(local).Append(" = ")
.Append(defaultExpr).Append(";").AppendLine();
argNames.Add($"{RefKindKeyword(p.RefKind)}{local}");
}
else
{
argNames.Add(defaultExpr);
}
}

sb.Append("\t\t\t").Append(backingFieldAccess).Append("?.Invoke(");

if (@event.Delegate.Parameters.Count > 0)
{
sb.Append(string.Join(", ",
@event.Delegate.Parameters.Select(p
=> $"mockBehavior.DefaultValue.Generate(default({p.Type.Fullname.TrimEnd('?')}))")));
sb.Append(string.Join(", ", argNames));
}

sb.Append(");").AppendLine();
Expand Down
19 changes: 15 additions & 4 deletions Source/Mockolate.SourceGenerators/Sources/Sources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,16 +395,27 @@ private static string FormatIndexerParametersAsNames(EquatableArray<MethodParame
=> string.Join(", ", parameters.Select(p => p.Name));

/// <summary>
/// Formats parameters with type and name (e.g., "int value, string name").
/// Formats parameters with type and name (e.g., "int value, ref string name"), preserving any
/// ref/out/in modifier so the generated declaration matches the original delegate signature.
/// </summary>
private static string FormatParametersWithTypeAndName(IEnumerable<MethodParameter> parameters)
=> string.Join(", ", parameters.Select(p => $"{p.Type.Fullname} {p.Name}"));
=> string.Join(", ", parameters.Select(p => $"{RefKindKeyword(p.RefKind)}{p.Type.Fullname} {p.Name}"));

/// <summary>
/// Formats parameters as names only (e.g., "value1, value2").
/// Formats parameters as names only (e.g., "ref value1, out value2"), preserving any
/// ref/out/in modifier so the generated invocation argument list matches the delegate signature.
/// </summary>
private static string FormatParametersAsNames(IEnumerable<MethodParameter> parameters)
=> string.Join(", ", parameters.Select(p => p.Name));
=> string.Join(", ", parameters.Select(p => $"{RefKindKeyword(p.RefKind)}{p.Name}"));

private static string RefKindKeyword(RefKind kind) => kind switch
{
RefKind.Ref => "ref ",
RefKind.Out => "out ",
RefKind.In => "in ",
RefKind.RefReadOnlyParameter => "ref readonly ",
_ => "",
};

/// <summary>
/// Appends a NamedParameter with nullable handling.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,33 @@ await That(result.Sources["Mock.MyService__IMyOtherService.g.cs"])
}
""").IgnoringNewlineStyle();
}

[Fact]
public async Task EventWithRefOutDelegate_ShouldPreserveModifiersOnRaise()
{
GeneratorResult result = Generator
.Run("""
using Mockolate;

namespace MyCode;
public class Program
{
public static void Main(string[] args)
{
_ = IMyService.CreateMock();
}
}

public delegate void MyEventDelegate(ref int value, out string message);

public interface IMyService
{
event MyEventDelegate SomeEvent;
}
""");

await That(result.Diagnostics).IsEmpty();
}
}
}
}