diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs index 4f8cd84..b0814c9 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs @@ -297,9 +297,17 @@ private static Dictionary FindAndBuildSetupReplacements( } /// - /// When is Returns with more than one argument, splits it - /// into a chain of single-argument calls (Mockolate has no multi-arg overload). Returns - /// when no rewrite is needed. + /// Tries to build an outer-level replacement for the configurator chain. Two cases trigger this: + /// + /// Multi-arg Returns/Throws — split into a chain of single-arg calls. + /// + /// ReturnsForAnyArgs/ThrowsForAnyArgs — append .AnyParameters() to the setup + /// receiver and rename the configurator to its non-ForAnyArgs form (also splitting when + /// multi-arg). + /// + /// + /// Returns when the original single-arg Returns/Throws/etc. shape can + /// be preserved by the inner-only rewrite. /// private static bool TryBuildSequentialOuter(InvocationExpressionSyntax outerInvocation, string configuratorMethod, ExpressionSyntax setupReceiver, @@ -307,31 +315,91 @@ private static bool TryBuildSequentialOuter(InvocationExpressionSyntax outerInvo { replacement = null; - if (configuratorMethod is not "Returns") + string? targetMethod; + bool injectAnyParameters; + switch (configuratorMethod) { - return false; + case "Returns": + targetMethod = "Returns"; + injectAnyParameters = false; + break; + case "Throws": + targetMethod = "Throws"; + injectAnyParameters = false; + break; + case "ReturnsForAnyArgs": + targetMethod = "Returns"; + injectAnyParameters = true; + break; + case "ThrowsForAnyArgs": + targetMethod = "Throws"; + injectAnyParameters = true; + break; + default: + return false; } - if (outerInvocation.ArgumentList.Arguments.Count <= 1) + // Inner-only rewrite still works for single-arg Returns/Throws with no AnyParameters injection. + if (!injectAnyParameters && outerInvocation.ArgumentList.Arguments.Count <= 1) { return false; } ExpressionSyntax current = setupReceiver; - foreach (ArgumentSyntax arg in outerInvocation.ArgumentList.Arguments) + if (injectAnyParameters) { current = SyntaxFactory.InvocationExpression( SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, current, - SyntaxFactory.IdentifierName(configuratorMethod)), - SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arg))); + SyntaxFactory.IdentifierName("AnyParameters")), + SyntaxFactory.ArgumentList()); + } + + IReadOnlyList outerArgs = outerInvocation.ArgumentList.Arguments; + if (outerArgs.Count == 0) + { + // e.g. ThrowsForAnyArgs() with no value argument — preserve the trailing call shape with no args. + current = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + current, + RenameConfiguratorIdentifier(outerInvocation, targetMethod)), + SyntaxFactory.ArgumentList()); + } + else + { + foreach (ArgumentSyntax arg in outerArgs) + { + current = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + current, + RenameConfiguratorIdentifier(outerInvocation, targetMethod)), + SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(arg))); + } } replacement = ((InvocationExpressionSyntax)current).WithTriviaFrom(outerInvocation); return true; } + /// + /// Returns the outer call's identifier renamed to , preserving any explicit + /// type-argument list (so Throws<E>() stays generic when migrating from + /// ThrowsForAnyArgs<E>()). + /// + private static SimpleNameSyntax RenameConfiguratorIdentifier(InvocationExpressionSyntax outerInvocation, string targetName) + { + if (outerInvocation.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax generic, }) + { + return SyntaxFactory.GenericName(SyntaxFactory.Identifier(targetName)) + .WithTypeArgumentList(generic.TypeArgumentList); + } + + return SyntaxFactory.IdentifierName(targetName); + } + /// /// Translates sub.When(x => x.Method(args)).Do(callback) to /// sub.Mock.Setup.Method(args).Do(callback), and diff --git a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.SetupTests.cs b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.SetupTests.cs index 4fd64d1..e2531f7 100644 --- a/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.SetupTests.cs +++ b/Tests/Mockolate.Migration.Tests/NSubstituteCodeFixProviderTests.SetupTests.cs @@ -275,6 +275,72 @@ public void Test() } """); + [Fact] + public async Task ReturnsForAnyArgs_AddsAnyParametersAndRenamesToReturns() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IFoo { int Bar(int x, string y); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Bar(default, default).ReturnsForAnyArgs(42); + } + } + """, + """ + using NSubstitute; + using Mockolate; + + public interface IFoo { int Bar(int x, string y); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Setup.Bar(default, default).AnyParameters().Returns(42); + } + } + """); + + [Fact] + public async Task ReturnsForAnyArgsSequential_SplitsAndKeepsAnyParameters() + => await Verifier.VerifyCodeFixAsync( + """ + using NSubstitute; + + public interface IFoo { int Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Bar(default).ReturnsForAnyArgs(1, 2, 3); + } + } + """, + """ + using NSubstitute; + using Mockolate; + + public interface IFoo { int Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Setup.Bar(default).AnyParameters().Returns(1).Returns(2).Returns(3); + } + } + """); + [Fact] public async Task SequentialPropertyReturns_AreSplitIntoChain() => await Verifier.VerifyCodeFixAsync( @@ -340,5 +406,116 @@ public void Test() } } """); + + [Fact] + public async Task ThrowsForAnyArgsGeneric_AddsAnyParametersAndRenamesToThrows() + => await Verifier.VerifyCodeFixAsync( + """ + using System; + using NSubstitute; + using NSubstitute.ExceptionExtensions; + + public interface IFoo { int Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Bar(default).ThrowsForAnyArgs(); + } + } + """, + """ + using System; + using NSubstitute; + using NSubstitute.ExceptionExtensions; + using Mockolate; + + public interface IFoo { int Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Setup.Bar(default).AnyParameters().Throws(); + } + } + """); + + [Fact] + public async Task ThrowsForAnyArgsSequential_SplitsAndKeepsAnyParameters() + => await Verifier.VerifyCodeFixAsync( + """ + using System; + using NSubstitute; + using NSubstitute.ExceptionExtensions; + + public interface IFoo { int Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Bar(default).ThrowsForAnyArgs(new InvalidOperationException(), new ArgumentException()); + } + } + """, + """ + using System; + using NSubstitute; + using NSubstitute.ExceptionExtensions; + using Mockolate; + + public interface IFoo { int Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Setup.Bar(default).AnyParameters().Throws(new InvalidOperationException()).Throws(new ArgumentException()); + } + } + """); + + [Fact] + public async Task ThrowsForAnyArgsWithException_AddsAnyParametersAndRenamesToThrows() + => await Verifier.VerifyCodeFixAsync( + """ + using System; + using NSubstitute; + using NSubstitute.ExceptionExtensions; + + public interface IFoo { int Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = [|Substitute.For()|]; + sub.Bar(default).ThrowsForAnyArgs(new InvalidOperationException()); + } + } + """, + """ + using System; + using NSubstitute; + using NSubstitute.ExceptionExtensions; + using Mockolate; + + public interface IFoo { int Bar(int x); } + + public class Tests + { + public void Test() + { + var sub = IFoo.CreateMock(); + sub.Mock.Setup.Bar(default).AnyParameters().Throws(new InvalidOperationException()); + } + } + """); } }