diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs index af25ccb..3bf9b1e 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs @@ -523,7 +523,8 @@ private static Dictionary 0) + { + replacement = replacement.WithLeadingTrivia(BuildNestedMockTodoTrivia(invocation, receiver)); + } + result[invocation] = replacement; } @@ -1279,6 +1310,11 @@ private static Dictionary 0) + { + replacement = replacement.WithLeadingTrivia(BuildNestedMockTodoTrivia(invocation, receiver)); + } + result[invocation] = replacement; } @@ -1796,6 +1832,24 @@ private static UsingDirectiveSyntax BuildUsingDirective(CompilationUnitSyntax co return SyntaxFactory.UsingDirective(name); } + private static SyntaxTriviaList BuildNestedMockTodoTrivia(SyntaxNode anchor, ExpressionSyntax navigationRoot) + { + SyntaxTriviaList existing = anchor.GetLeadingTrivia(); + SyntaxTrivia indent = existing.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); + string endOfLine = DetectLineEnding(anchor.SyntaxTree.GetRoot()); + return existing + .Add(SyntaxFactory.Comment( + $"// TODO(MockolateM001): register the nested '{navigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)")) + .Add(SyntaxFactory.EndOfLine(endOfLine)) + .Add(indent); + } + + private static string DetectLineEnding(SyntaxNode root) => + root.DescendantTrivia(descendIntoTrivia: true) + .Where(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) + .Select(t => t.ToFullString()) + .FirstOrDefault(s => s.Length > 0) ?? "\n"; + private static TypeSyntax? GetTypeArgumentFromSemanticModel( SemanticModel? semanticModel, ExpressionSyntax expressionSyntax, diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs index 9d93a19..fac4689 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs @@ -318,7 +318,7 @@ private static Dictionary FindAndBuildSetupReplacements( if (outerReplacement is not null) { - outerReplacement = ApplySetupTrivia(outerReplacement, outerInvocation, + outerReplacement = ApplyTodoTrivia(outerReplacement, outerInvocation, isNested ? targetMemberAccess.Expression : null, callInfoTodoNeeded); result[outerInvocation] = outerReplacement; } @@ -363,7 +363,7 @@ private static Dictionary FindAndBuildSetupReplacements( if (outerPropertyReplacement is not null) { - outerPropertyReplacement = ApplySetupTrivia(outerPropertyReplacement, outerInvocation, + outerPropertyReplacement = ApplyTodoTrivia(outerPropertyReplacement, outerInvocation, isNestedProperty ? targetPropertyAccess.Expression : null, propertyCallInfoTodoNeeded); result[outerInvocation] = outerPropertyReplacement; } @@ -427,28 +427,30 @@ private static (InvocationExpressionSyntax effectiveOuter, bool callInfoTodoNeed } /// - /// Combines the nested-mock and CallInfo TODO comments onto a single setup replacement when both apply. - /// Either flag may be inactive; if neither is active, the replacement is returned untouched. + /// Layers the nested-mock and/or CallInfo TODO comments onto a migrated replacement. Called from + /// every rewrite path that can produce these TODOs — setup-call, setup-property, method verify, + /// property verify, and event raise. Either flag may be inactive; if neither is active, the + /// replacement is returned untouched. /// - private static InvocationExpressionSyntax ApplySetupTrivia(InvocationExpressionSyntax replacement, - InvocationExpressionSyntax outerInvocation, ExpressionSyntax? nestedNavigationRoot, bool callInfoTodoNeeded) + private static InvocationExpressionSyntax ApplyTodoTrivia(InvocationExpressionSyntax replacement, + SyntaxNode anchor, ExpressionSyntax? nestedNavigationRoot, bool callInfoTodoNeeded) { if (nestedNavigationRoot is null && !callInfoTodoNeeded) { return replacement; } - SyntaxTriviaList trivia = outerInvocation.GetLeadingTrivia(); + SyntaxTriviaList trivia = anchor.GetLeadingTrivia(); if (nestedNavigationRoot is not null) { - trivia = AppendTodoComment(trivia, outerInvocation, - $"// TODO: register the nested '{nestedNavigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)"); + trivia = AppendTodoComment(trivia, anchor, + $"// TODO(MockolateM002): register the nested '{nestedNavigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)"); } if (callInfoTodoNeeded) { - trivia = AppendTodoComment(trivia, outerInvocation, - "// TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo"); + trivia = AppendTodoComment(trivia, anchor, + "// TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo"); } return replacement.WithLeadingTrivia(trivia); @@ -475,7 +477,7 @@ private static InvocationExpressionSyntax BuildSimpleOuter(ExpressionSyntax setu /// private static SyntaxTriviaList BuildCallInfoTodoTrivia(SyntaxNode anchor) => AppendTodoComment(anchor.GetLeadingTrivia(), anchor, - "// TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo"); + "// TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo"); private static SyntaxTriviaList AppendTodoComment(SyntaxTriviaList existingLeading, SyntaxNode anchor, string commentText) @@ -823,8 +825,14 @@ private static Dictionary await Verifier.VerifyCodeFixAsync( + """ + using Moq; + + public interface IBar { bool Bar(string x); } + public interface IFoo { IBar Child { get; } } + + public class Tests + { + public void Test() + { + var mock = [|new Mock()|]; + mock.Setup(m => m.Child.Bar(It.IsAny())) + .Returns(true) + .Callback(x => { }); + } + } + """, + """ + using Moq; + using Mockolate; + + public interface IBar { bool Bar(string x); } + public interface IFoo { IBar Child { get; } } + + public class Tests + { + public void Test() + { + var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + mock.Child.Mock.Bar(It.IsAny()) + .Returns(true) + .Do(x => { }); + } + } + """); } } diff --git a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.NewMockTests.cs b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.NewMockTests.cs index de24452..f0f692a 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.NewMockTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.NewMockTests.cs @@ -222,6 +222,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Child.GrandChild' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) mock.Child.GrandChild.Mock.Bar(It.IsAny()).Returns(true); } } diff --git a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.PropertyTests.cs b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.PropertyTests.cs index 57ce24b..0a6e27a 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.PropertyTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.PropertyTests.cs @@ -343,6 +343,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) mock.Child.Mock.Verify.Name.Got().Exactly(2); } } diff --git a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs index c32dea1..ce1330c 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs @@ -558,6 +558,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Bar.Baz' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) mock.Bar.Baz.Mock.Setup.Name.Returns("baz"); } } @@ -593,6 +594,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Bar' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) mock.Bar.Mock.Setup.Name.InitializeWith("value"); } } @@ -628,6 +630,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Bar' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) mock.Bar.Mock.Setup.Name.Register(); } } @@ -807,6 +810,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) mock.Child.Mock.GetCount() .Returns(1) .Throws(); diff --git a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyEventTests.cs b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyEventTests.cs index 5a8cfe8..8416f26 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyEventTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyEventTests.cs @@ -111,6 +111,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) mock.Child.Mock.Verify.MyEvent.Subscribed().Never(); } } diff --git a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyTests.cs b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyTests.cs index c73b21a..70de30d 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyTests.cs @@ -72,6 +72,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); + // TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) mock.Child.Mock.Verify.Compute(It.IsAny()).Once(); } } diff --git a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.CallInfoTests.cs b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.CallInfoTests.cs index d915009..158bf0c 100644 --- a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.CallInfoTests.cs +++ b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.CallInfoTests.cs @@ -39,7 +39,7 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo + // TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo sub.Mock.Setup.Bar(0, 0).Returns(0).Do(call => Console.WriteLine(call.Arg())); } } @@ -73,7 +73,7 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo + // TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo sub.Mock.Setup.TryGet("k", out _).Returns(true).Do(call => { call[1] = 42; }); } } @@ -393,7 +393,7 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo + // TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo sub.Mock.Setup.Bar(0).Returns(call => Helper(call)); } } @@ -431,7 +431,7 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo + // TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo sub.Mock.Setup.Bar(0).Returns(call => { foreach (var x in new[] { 1, 2 }) { _ = x; } @@ -474,7 +474,7 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo + // TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo sub.Mock.Setup.Dispense("Dark", 1).Returns(call => { string type = call.Arg(); @@ -519,7 +519,7 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo + // TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo sub.Mock.Setup.Bar(0).Returns(call => { Action a = (x) => Console.WriteLine(x); diff --git a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.NestedTests.cs b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.NestedTests.cs index 9a8d222..30a4aac 100644 --- a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.NestedTests.cs +++ b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.NestedTests.cs @@ -37,7 +37,7 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: register the nested 'sub.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // TODO(MockolateM002): register the nested 'sub.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) sub.Child.Mock.Setup.Compute(1).Returns(42); } } @@ -73,12 +73,86 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: register the nested 'sub.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // TODO(MockolateM002): register the nested 'sub.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) sub.Child.Mock.Setup.Name.Returns("baz"); } } """); + [Fact] + public async Task NestedReceivedProperty_Got_RewritesAndAddsTodo() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IBar { string Name { get; } } + public interface IFoo { IBar Child { get; } } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + _ = sub.Child.Received(2).Name; + } + } + """, + """ + using NSubstitute; + using Mockolate; + using Mockolate.Verify; + + public interface IBar { string Name { get; } } + public interface IFoo { IBar Child { get; } } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + // TODO(MockolateM002): register the nested 'sub.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + sub.Child.Mock.Verify.Name.Got().Exactly(2); + } + } + """); + + [Fact] + public async Task NestedReceivedProperty_Set_RewritesAndAddsTodo() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IBar { string Name { get; set; } } + public interface IFoo { IBar Child { get; } } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Child.Received(2).Name = "baz"; + } + } + """, + """ + using NSubstitute; + using Mockolate; + using Mockolate.Verify; + + public interface IBar { string Name { get; set; } } + public interface IFoo { IBar Child { get; } } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + // TODO(MockolateM002): register the nested 'sub.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + sub.Child.Mock.Verify.Name.Set("baz").Exactly(2); + } + } + """); + [Fact] public async Task NestedReceivedMethod_RewritesToVerifyOnNestedMock() => await Verifier.VerifyCodeFixAsync( @@ -110,9 +184,48 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); + // TODO(MockolateM002): register the nested 'sub.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) sub.Child.Mock.Verify.Compute(1).Exactly(2); } } """); + + [Fact] + public async Task NestedRaiseEvent_RewritesAndAddsTodo() + => await Verifier.VerifyCodeFixAsync( + """ + using System; + using NSubstitute; + + public interface IBar { event EventHandler MyEvent; } + public interface IFoo { IBar Child { get; } } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Child.MyEvent += Raise.EventWith(EventArgs.Empty); + } + } + """, + """ + using System; + using NSubstitute; + using Mockolate; + + public interface IBar { event EventHandler MyEvent; } + public interface IFoo { IBar Child { get; } } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + // TODO(MockolateM002): register the nested 'sub.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + sub.Child.Mock.Raise.MyEvent(null, EventArgs.Empty); + } + } + """); } } diff --git a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.WhenDoTests.cs b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.WhenDoTests.cs index 9e10d0e..e9c77a2 100644 --- a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.WhenDoTests.cs +++ b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.WhenDoTests.cs @@ -105,7 +105,7 @@ public class Tests public void Test() { var sub = IFoo.CreateMock(); - // TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo + // TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo sub.Mock.Setup.Bar(1).Do(call => Console.WriteLine(call)); } }