Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,14 @@ protected override async Task<Document> ConvertAssertionAsync(CodeFixContext con
Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax> clearReplacements =
FindAndBuildClearReceivedCallsReplacements(allInvocations, semanticModel, mockSymbol, cancellationToken);

Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax> raiseReplacements =
FindAndBuildRaiseReplacements(compilationUnit, semanticModel, mockSymbol, cancellationToken);

List<SyntaxNode> nodesToReplace = [substituteCall,];
nodesToReplace.AddRange(setupReplacements.Keys);
nodesToReplace.AddRange(verifyReplacements.Keys);
nodesToReplace.AddRange(clearReplacements.Keys);
nodesToReplace.AddRange(raiseReplacements.Keys);

compilationUnit = compilationUnit.ReplaceNodes(
nodesToReplace,
Expand Down Expand Up @@ -103,6 +107,12 @@ protected override async Task<Document> ConvertAssertionAsync(CodeFixContext con
}
}

if (original is AssignmentExpressionSyntax assignment &&
raiseReplacements.TryGetValue(assignment, out InvocationExpressionSyntax? raiseReplacement))
{
return raiseReplacement;
}

return original;
});

Expand Down Expand Up @@ -245,6 +255,122 @@ private static Dictionary<SyntaxNode, SyntaxNode> FindAndBuildSetupReplacements(
return result;
}

private static Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax> FindAndBuildRaiseReplacements(
CompilationUnitSyntax compilationUnit,
SemanticModel? semanticModel,
ISymbol? mockSymbol,
CancellationToken cancellationToken)
{
if (semanticModel is null || mockSymbol is null)
{
return [];
}

Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax> result = [];

foreach (AssignmentExpressionSyntax assignment in compilationUnit.DescendantNodes().OfType<AssignmentExpressionSyntax>())
{
if (!assignment.IsKind(SyntaxKind.AddAssignmentExpression))
{
continue;
}

if (assignment.Left is not MemberAccessExpressionSyntax eventAccess ||
assignment.Right is not InvocationExpressionSyntax raiseInvocation)
{
continue;
}

Comment thread
vbreuss marked this conversation as resolved.
if (!IsTrackedMockReceiver(eventAccess.Expression, semanticModel, mockSymbol, cancellationToken))
{
continue;
}

if (raiseInvocation.Expression is not MemberAccessExpressionSyntax raiseAccess ||
raiseAccess.Expression is not IdentifierNameSyntax { Identifier.Text: "Raise", })
{
continue;
}

string raiseMethod = raiseAccess.Name.Identifier.Text;
Comment thread
vbreuss marked this conversation as resolved.
Outdated
ArgumentListSyntax raiseArgs = BuildRaiseArguments(raiseInvocation.ArgumentList, raiseMethod);

MemberAccessExpressionSyntax mockAccess = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
eventAccess.Expression,
SyntaxFactory.IdentifierName("Mock"));
MemberAccessExpressionSyntax raiseMember = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
mockAccess,
SyntaxFactory.IdentifierName("Raise"));
MemberAccessExpressionSyntax raiseEventName = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
raiseMember,
eventAccess.Name.WithoutTrivia());

result[assignment] = SyntaxFactory.InvocationExpression(raiseEventName, raiseArgs)
.WithTriviaFrom(assignment);
}

return result;
}

/// <summary>
/// Translates the argument list of an NSubstitute <c>Raise.X(...)</c> call into the corresponding
/// <c>Mock.Raise.EventName(...)</c> argument list.
/// </summary>
private static ArgumentListSyntax BuildRaiseArguments(ArgumentListSyntax raiseArgs, string raiseMethod)
{
// Raise.Event<TDelegate>(args...) — non-EventHandler delegates, just forward the args.
// Raise.EventWith(args) — single arg means EventArgs only (sender omitted, defaults to null).
// Raise.EventWith(sender, ea) — two args, pass through.
// Raise.Event() / Raise.EventWith() — empty, default to (null, EventArgs.Empty) for EventHandler.
if (raiseMethod is "Event")
{
if (raiseArgs.Arguments.Count == 0)
{
return SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(
[
SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)),
SyntaxFactory.Argument(SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("EventArgs"),
SyntaxFactory.IdentifierName("Empty"))),
]));
}

return raiseArgs;
}

if (raiseMethod is "EventWith")
{
if (raiseArgs.Arguments.Count == 0)
{
return SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(
[
SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)),
SyntaxFactory.Argument(SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("EventArgs"),
SyntaxFactory.IdentifierName("Empty"))),
]));
Comment thread
vbreuss marked this conversation as resolved.
}

if (raiseArgs.Arguments.Count == 1)
{
return SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(
[
SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)),
raiseArgs.Arguments[0],
]));
}

return raiseArgs;
}

return raiseArgs;
}

private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax> FindAndBuildClearReceivedCallsReplacements(
IReadOnlyList<InvocationExpressionSyntax> allInvocations,
SemanticModel? semanticModel,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using Verifier = Mockolate.Migration.Tests.Verifiers.CSharpCodeFixVerifier<Mockolate.Migration.Analyzers.NSubstituteAnalyzer,
Mockolate.Migration.Analyzers.NSubstituteCodeFixProvider>;

namespace Mockolate.Migration.Tests;

public partial class NSubstituteCodeFixProviderTests
{
public sealed class RaiseTests
{
[Fact]
public async Task RaiseEvent_DelegateType_ForwardsArgsWithoutType()
=> await Verifier.VerifyCodeFixAsync(
"""
using System;
using NSubstitute;

public interface IFoo { event Action<int> MyEvent; }

public class Tests
{
public void Test()
{
var sub = [|Substitute.For<IFoo>()|];
sub.MyEvent += Raise.Event<Action<int>>(123);
}
}
""",
"""
using System;
using NSubstitute;
using Mockolate;

public interface IFoo { event Action<int> MyEvent; }

public class Tests
{
public void Test()
{
var sub = IFoo.CreateMock();
sub.Mock.Raise.MyEvent(123);
}
}
""");

[Fact]
public async Task RaiseEvent_NoArgs_RewritesToNullAndEmpty()
=> await Verifier.VerifyCodeFixAsync(
"""
using System;
using NSubstitute;

public interface IFoo { event EventHandler MyEvent; }

public class Tests
{
public void Test()
{
var sub = [|Substitute.For<IFoo>()|];
sub.MyEvent += Raise.Event();
}
}
""",
"""
using System;
using NSubstitute;
using Mockolate;

public interface IFoo { event EventHandler MyEvent; }

public class Tests
{
public void Test()
{
var sub = IFoo.CreateMock();
sub.Mock.Raise.MyEvent(null, EventArgs.Empty);
}
}
""");

[Fact]
Comment thread
vbreuss marked this conversation as resolved.
public async Task RaiseEventWith_ArgsOnly_PrependsNullSender()
=> await Verifier.VerifyCodeFixAsync(
"""
using System;
using NSubstitute;

public class MyArgs : EventArgs { }
public interface IFoo { event EventHandler<MyArgs> MyEvent; }

public class Tests
{
public void Test()
{
var sub = [|Substitute.For<IFoo>()|];
sub.MyEvent += Raise.EventWith(new MyArgs());
}
}
""",
"""
using System;
using NSubstitute;
using Mockolate;

public class MyArgs : EventArgs { }
public interface IFoo { event EventHandler<MyArgs> MyEvent; }

public class Tests
{
public void Test()
{
var sub = IFoo.CreateMock();
sub.Mock.Raise.MyEvent(null, new MyArgs());
}
}
""");

[Fact]
public async Task RaiseEventWith_SenderAndArgs_PassesThrough()
=> await Verifier.VerifyCodeFixAsync(
"""
using System;
using NSubstitute;

public interface IFoo { event EventHandler MyEvent; }

public class Tests
{
public void Test()
{
var sub = [|Substitute.For<IFoo>()|];
sub.MyEvent += Raise.EventWith(this, EventArgs.Empty);
}
}
""",
"""
using System;
using NSubstitute;
using Mockolate;

public interface IFoo { event EventHandler MyEvent; }

public class Tests
{
public void Test()
{
var sub = IFoo.CreateMock();
sub.Mock.Raise.MyEvent(this, EventArgs.Empty);
}
}
""");
}
}
Loading