diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs index bfdb987..8400791 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs @@ -65,8 +65,12 @@ protected override async Task ConvertAssertionAsync(CodeFixContext con Dictionary setupReplacements = FindAndBuildSetupReplacements(allInvocations, semanticModel, mockSymbol, cancellationToken); + Dictionary verifyReplacements = + FindAndBuildVerifyReplacements(allInvocations, semanticModel, mockSymbol, cancellationToken); + List nodesToReplace = [substituteCall,]; nodesToReplace.AddRange(setupReplacements.Keys); + nodesToReplace.AddRange(verifyReplacements.Keys); compilationUnit = compilationUnit.ReplaceNodes( nodesToReplace, @@ -77,9 +81,15 @@ protected override async Task ConvertAssertionAsync(CodeFixContext con return creationReplacement.WithTriviaFrom(substituteCall); } - if (setupReplacements.TryGetValue(original, out SyntaxNode? replacement)) + if (setupReplacements.TryGetValue(original, out SyntaxNode? setupReplacement)) { - return replacement; + return setupReplacement; + } + + if (original is InvocationExpressionSyntax invocation && + verifyReplacements.TryGetValue(invocation, out InvocationExpressionSyntax? verifyReplacement)) + { + return verifyReplacement; } return original; @@ -92,6 +102,16 @@ protected override async Task ConvertAssertionAsync(CodeFixContext con compilationUnit = compilationUnit.AddUsings(usingDirective); } + if (verifyReplacements.Count > 0) + { + bool hasVerifyUsing = compilationUnit.Usings.Any(u => u.Name?.ToString() == "Mockolate.Verify"); + if (!hasVerifyUsing) + { + UsingDirectiveSyntax verifyUsingDirective = BuildUsingDirective(compilationUnit, "Mockolate.Verify"); + compilationUnit = compilationUnit.AddUsings(verifyUsingDirective); + } + } + return document.WithSyntaxRoot(compilationUnit); } @@ -214,6 +234,111 @@ private static Dictionary FindAndBuildSetupReplacements( return result; } + private static Dictionary FindAndBuildVerifyReplacements( + IReadOnlyList allInvocations, + SemanticModel? semanticModel, + ISymbol? mockSymbol, + CancellationToken cancellationToken) + { + if (semanticModel is null || mockSymbol is null) + { + return []; + } + + Dictionary result = []; + + foreach (InvocationExpressionSyntax outerInvocation in allInvocations) + { + if (outerInvocation.Expression is not MemberAccessExpressionSyntax outerAccess) + { + continue; + } + + // outerInvocation is `something.MethodName(args)`; receiver `something` should be `sub.Received(...)` + // (or DidNotReceive). The receiver of `sub.Received()` is the tracked mock symbol. + if (outerAccess.Expression is not InvocationExpressionSyntax receiverCall || + receiverCall.Expression is not MemberAccessExpressionSyntax receiverAccess) + { + continue; + } + + string receivedMethod = receiverAccess.Name.Identifier.Text; + if (receivedMethod is not ("Received" or "DidNotReceive")) + { + continue; + } + + if (!IsTrackedMockReceiver(receiverAccess.Expression, semanticModel, mockSymbol, cancellationToken)) + { + continue; + } + + ExpressionSyntax mockReceiver = receiverAccess.Expression; + ArgumentListSyntax transformedArgs = + TransformNSubstituteArgReferences(outerInvocation.ArgumentList, semanticModel, cancellationToken); + SimpleNameSyntax methodNameSyntax = outerAccess.Name; + + MemberAccessExpressionSyntax verifyAccess = BuildVerifyAccess(mockReceiver, methodNameSyntax); + InvocationExpressionSyntax verifyInvocation = SyntaxFactory.InvocationExpression(verifyAccess, transformedArgs); + + InvocationExpressionSyntax suffix = BuildVerifySuffix(verifyInvocation, receivedMethod, receiverCall.ArgumentList); + + result[outerInvocation] = suffix.WithTriviaFrom(outerInvocation); + } + + return result; + } + + private static MemberAccessExpressionSyntax BuildVerifyAccess(ExpressionSyntax receiver, SimpleNameSyntax memberName) + { + MemberAccessExpressionSyntax mockAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + receiver, + SyntaxFactory.IdentifierName("Mock")); + MemberAccessExpressionSyntax verifyMember = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + mockAccess, + SyntaxFactory.IdentifierName("Verify")); + return SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + verifyMember, + memberName); + } + + private static InvocationExpressionSyntax BuildVerifySuffix(InvocationExpressionSyntax verifyInvocation, + string receivedMethod, ArgumentListSyntax receivedArgs) + { + // DidNotReceive() → .Never(); Received() → .AtLeastOnce(); Received(n) → .Exactly(n) or .Once() when n is 1. + if (receivedMethod == "DidNotReceive") + { + return AppendCountCall(verifyInvocation, "Never", SyntaxFactory.ArgumentList()); + } + + if (receivedArgs.Arguments.Count == 0) + { + return AppendCountCall(verifyInvocation, "AtLeastOnce", SyntaxFactory.ArgumentList()); + } + + // Received(n): when n is the literal integer 1 we collapse to Once(); otherwise pass through to Exactly(n). + if (receivedArgs.Arguments.Count == 1 && + receivedArgs.Arguments[0].Expression is LiteralExpressionSyntax literal && + literal.Token.Value is 1) + { + return AppendCountCall(verifyInvocation, "Once", SyntaxFactory.ArgumentList()); + } + + return AppendCountCall(verifyInvocation, "Exactly", receivedArgs); + } + + private static InvocationExpressionSyntax AppendCountCall(InvocationExpressionSyntax verifyInvocation, + string methodName, ArgumentListSyntax argList) => + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + verifyInvocation, + SyntaxFactory.IdentifierName(methodName)), + argList); + /// /// Returns when ultimately resolves to the tracked /// mock symbol — either directly or via a chain of property/field accesses (auto-mocked nested members). diff --git a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.VerifyTests.cs b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.VerifyTests.cs new file mode 100644 index 0000000..0282aab --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.VerifyTests.cs @@ -0,0 +1,182 @@ +using Verifier = Mockolate.Migration.Tests.Verifiers.CSharpCodeFixVerifier; + +namespace Mockolate.Migration.Tests; + +public partial class NSubstituteCodeFixProviderTests +{ + public sealed class VerifyTests + { + [Fact] + public async Task DidNotReceive_IsRewrittenToNever() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.DidNotReceive().Bar(1); + } + } + """, + """ + using NSubstitute; + using Mockolate; + using Mockolate.Verify; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Verify.Bar(1).Never(); + } + } + """); + + [Fact] + public async Task Received_IsRewrittenToAtLeastOnce() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Bar(1); + sub.Received().Bar(1); + } + } + """, + """ + using NSubstitute; + using Mockolate; + using Mockolate.Verify; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Bar(1); + sub.Mock.Verify.Bar(1).AtLeastOnce(); + } + } + """); + + [Fact] + public async Task ReceivedExactCount_IsRewrittenToExactly() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Received(3).Bar(1); + } + } + """, + """ + using NSubstitute; + using Mockolate; + using Mockolate.Verify; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Verify.Bar(1).Exactly(3); + } + } + """); + + [Fact] + public async Task ReceivedOne_IsRewrittenToOnce() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Received(1).Bar(1); + } + } + """, + """ + using NSubstitute; + using Mockolate; + using Mockolate.Verify; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Verify.Bar(1).Once(); + } + } + """); + + [Fact] + public async Task ReceivedWithArgMatcher_TransformsMatcher() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Received().Bar(Arg.Any()); + } + } + """, + """ + using NSubstitute; + using Mockolate; + using Mockolate.Verify; + + public interface IFoo { void Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Verify.Bar(It.IsAny()).AtLeastOnce(); + } + } + """); + } +}