From 48c0316c985ea51ea42e11d2bf1f11167d8fb958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 15 May 2026 08:54:33 +0200 Subject: [PATCH 1/3] feat: emit nested-mock TODO across all migration rewrite sites NSubstitute and Moq fixers silently rewrite nested receivers like sub.Child.Foo() into sub.Child.Mock.Setup/Verify/Raise.Foo() chains, which NRE at runtime if the nested child isn't explicitly registered (Mockolate doesn't auto-mock recursively). The setup path already emitted a TODO comment warning the user; this extends the same warning to every other rewrite site that builds a nested chain: - NSubstitute: method verify, property verify (Got/Set), event raise. - Moq: Setup call, Setup property access, SetupProperty, Verify call, VerifyAdd/VerifyRemove event, VerifyGet/VerifySet property. Also fixes a trivia-clobbering bug in the Moq Callback rewrite: WithTriviaFrom(invocation) on the wrapping Do() expression was overwriting the leading trivia of the embedded migrated setup, dropping any TODO attached to it. Switched to WithTrailingTrivia only so the leading trivia (with the TODO) is preserved. --- .../MoqCodeFixProvider.cs | 71 ++++++++++- .../NSubstituteCodeFixProvider.cs | 40 +++++-- .../MoqCodeFixProviderTests.CallbackTests.cs | 40 +++++++ .../MoqCodeFixProviderTests.NewMockTests.cs | 1 + .../MoqCodeFixProviderTests.PropertyTests.cs | 1 + .../MoqCodeFixProviderTests.SetupTests.cs | 4 + ...oqCodeFixProviderTests.VerifyEventTests.cs | 1 + .../MoqCodeFixProviderTests.VerifyTests.cs | 1 + ...stituteCodeFixProviderTests.NestedTests.cs | 113 ++++++++++++++++++ 9 files changed, 261 insertions(+), 11 deletions(-) diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs index af25ccb..823ef1c 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,35 @@ 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: 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) + { + foreach (SyntaxTrivia trivia in root.DescendantTrivia(descendIntoTrivia: true)) + { + if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + { + string text = trivia.ToFullString(); + if (text.Length > 0) + { + return text; + } + } + } + + return "\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..3f8a46c 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs @@ -431,23 +431,23 @@ private static (InvocationExpressionSyntax effectiveOuter, bool callInfoTodoNeed /// 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) + 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, + trivia = AppendTodoComment(trivia, anchor, $"// TODO: register the nested '{nestedNavigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)"); } if (callInfoTodoNeeded) { - trivia = AppendTodoComment(trivia, outerInvocation, + trivia = AppendTodoComment(trivia, anchor, "// TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo"); } @@ -823,8 +823,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: 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..03c97f5 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: 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..af16d50 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: 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..2b54496 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: 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: 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: 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: 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..0d87ee7 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: 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..537887b 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: 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.NestedTests.cs b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.NestedTests.cs index 9a8d222..5827841 100644 --- a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.NestedTests.cs +++ b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.NestedTests.cs @@ -79,6 +79,80 @@ public void Test() } """); + [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: 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: 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: 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: 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); + } + } + """); } } From a11513119e903e6b9bd82f82b2f74f8d7f8f2138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 15 May 2026 09:13:06 +0200 Subject: [PATCH 2/3] feat: tag migration TODOs with the originating analyzer rule ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated TODO comments now include the rule ID of the analyzer that triggered the codefix in brackets, so users can grep for or filter unresolved migration hints by source: // TODO(MockolateM001): register the nested 'mock.Child' chain ... // TODO(MockolateM002): review CallInfo usage manually ... Applies to every TODO emitted by NSubstituteCodeFixProvider (MockolateM002 — both the nested-mock and CallInfo TODOs) and MoqCodeFixProvider (MockolateM001 — nested-mock TODO). --- .../MoqCodeFixProvider.cs | 2 +- .../NSubstituteCodeFixProvider.cs | 6 +++--- .../MoqCodeFixProviderTests.CallbackTests.cs | 2 +- .../MoqCodeFixProviderTests.NewMockTests.cs | 2 +- .../MoqCodeFixProviderTests.PropertyTests.cs | 2 +- .../MoqCodeFixProviderTests.SetupTests.cs | 8 ++++---- .../MoqCodeFixProviderTests.VerifyEventTests.cs | 2 +- .../MoqCodeFixProviderTests.VerifyTests.cs | 2 +- .../NSubstituteCodeFixProviderTests.CallInfoTests.cs | 12 ++++++------ .../NSubstituteCodeFixProviderTests.NestedTests.cs | 12 ++++++------ .../NSubstituteCodeFixProviderTests.WhenDoTests.cs | 2 +- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs index 823ef1c..81debb5 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs @@ -1839,7 +1839,7 @@ private static SyntaxTriviaList BuildNestedMockTodoTrivia(SyntaxNode anchor, Exp string endOfLine = DetectLineEnding(anchor.SyntaxTree.GetRoot()); return existing .Add(SyntaxFactory.Comment( - $"// TODO: register the nested '{navigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)")) + $"// TODO(MockolateM001): register the nested '{navigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)")) .Add(SyntaxFactory.EndOfLine(endOfLine)) .Add(indent); } diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs index 3f8a46c..574b8ad 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs @@ -442,13 +442,13 @@ private static InvocationExpressionSyntax ApplySetupTrivia(InvocationExpressionS if (nestedNavigationRoot is not null) { trivia = AppendTodoComment(trivia, anchor, - $"// TODO: register the nested '{nestedNavigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)"); + $"// TODO(MockolateM002): register the nested '{nestedNavigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)"); } if (callInfoTodoNeeded) { trivia = AppendTodoComment(trivia, 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"); } return replacement.WithLeadingTrivia(trivia); @@ -475,7 +475,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) diff --git a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.CallbackTests.cs b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.CallbackTests.cs index 7300718..b4f0745 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.CallbackTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.CallbackTests.cs @@ -187,7 +187,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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 03c97f5..f0f692a 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.NewMockTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.NewMockTests.cs @@ -222,7 +222,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Child.GrandChild' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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 af16d50..0a6e27a 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.PropertyTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.PropertyTests.cs @@ -343,7 +343,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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 2b54496..ce1330c 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs @@ -558,7 +558,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Bar.Baz' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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"); } } @@ -594,7 +594,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Bar' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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"); } } @@ -630,7 +630,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Bar' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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(); } } @@ -810,7 +810,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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 0d87ee7..8416f26 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyEventTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyEventTests.cs @@ -111,7 +111,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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 537887b..70de30d 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.VerifyTests.cs @@ -72,7 +72,7 @@ public class Tests public void Test() { var mock = IFoo.CreateMock(); - // TODO: register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively) + // 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 5827841..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,7 +73,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.Name.Returns("baz"); } } @@ -110,7 +110,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.Verify.Name.Got().Exactly(2); } } @@ -147,7 +147,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.Verify.Name.Set("baz").Exactly(2); } } @@ -184,7 +184,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.Verify.Compute(1).Exactly(2); } } @@ -222,7 +222,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.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)); } } From a459f844fa1de3e36876c32d37a362106b757a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 15 May 2026 09:17:58 +0200 Subject: [PATCH 3/3] refactor: address review feedback on TODO trivia helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename ApplySetupTrivia → ApplyTodoTrivia and update its summary comment: the helper is no longer setup-specific, it is invoked from setup-call, setup-property, method verify, property verify, and event raise rewrite paths. - Simplify Moq DetectLineEnding to a LINQ Where/Select/FirstOrDefault expression (Sonar S3267). --- .../MoqCodeFixProvider.cs | 21 +++++-------------- .../NSubstituteCodeFixProvider.cs | 20 ++++++++++-------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs index 81debb5..3bf9b1e 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs @@ -1844,22 +1844,11 @@ private static SyntaxTriviaList BuildNestedMockTodoTrivia(SyntaxNode anchor, Exp .Add(indent); } - private static string DetectLineEnding(SyntaxNode root) - { - foreach (SyntaxTrivia trivia in root.DescendantTrivia(descendIntoTrivia: true)) - { - if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) - { - string text = trivia.ToFullString(); - if (text.Length > 0) - { - return text; - } - } - } - - return "\n"; - } + 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, diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs index 574b8ad..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,10 +427,12 @@ 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, + private static InvocationExpressionSyntax ApplyTodoTrivia(InvocationExpressionSyntax replacement, SyntaxNode anchor, ExpressionSyntax? nestedNavigationRoot, bool callInfoTodoNeeded) { if (nestedNavigationRoot is null && !callInfoTodoNeeded) @@ -827,7 +829,7 @@ private static Dictionary