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 @@ -297,41 +297,109 @@ private static Dictionary<SyntaxNode, SyntaxNode> FindAndBuildSetupReplacements(
}

/// <summary>
/// When <paramref name="configuratorMethod" /> is <c>Returns</c> with more than one argument, splits it
/// into a chain of single-argument calls (Mockolate has no multi-arg overload). Returns
/// <see langword="false" /> when no rewrite is needed.
/// Tries to build an outer-level replacement for the configurator chain. Three cases trigger this:
/// <list type="bullet">
/// <item>Multi-arg <c>Returns</c>/<c>Throws</c> — split into a chain of single-arg calls.</item>
/// <item>
/// <c>ReturnsForAnyArgs</c>/<c>ThrowsForAnyArgs</c> — append <c>.AnyParameters()</c> to the setup
/// receiver and rename the configurator to its non-<c>ForAnyArgs</c> form (also splitting when
/// multi-arg).
/// </item>
/// </list>
/// Returns <see langword="false" /> when the original single-arg <c>Returns</c>/<c>Throws</c>/etc. shape can
/// be preserved by the inner-only rewrite.
Comment thread
vbreuss marked this conversation as resolved.
Outdated
/// </summary>
private static bool TryBuildSequentialOuter(InvocationExpressionSyntax outerInvocation,
string configuratorMethod, ExpressionSyntax setupReceiver,
out InvocationExpressionSyntax? replacement)
{
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;
Comment thread
vbreuss marked this conversation as resolved.
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<ArgumentSyntax> outerArgs = outerInvocation.ArgumentList.Arguments;
if (outerArgs.Count == 0)
{
// e.g. ThrowsForAnyArgs<E>() 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.WithoutTrivia())));
Comment thread
vbreuss marked this conversation as resolved.
Outdated
}
}

replacement = ((InvocationExpressionSyntax)current).WithTriviaFrom(outerInvocation);
return true;
}

/// <summary>
/// Returns the outer call's identifier renamed to <paramref name="targetName" />, preserving any explicit
/// type-argument list (so <c>Throws&lt;E&gt;()</c> stays generic when migrating from
/// <c>ThrowsForAnyArgs&lt;E&gt;()</c>).
/// </summary>
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);
}

/// <summary>
/// Translates <c>sub.When(x =&gt; x.Method(args)).Do(callback)</c> to
/// <c>sub.Mock.Setup.Method(args).Do(callback)</c>, and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IFoo>()|];
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<IFoo>()|];
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(
Expand Down Expand Up @@ -340,5 +406,42 @@ 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<IFoo>()|];
sub.Bar(default).ThrowsForAnyArgs<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<InvalidOperationException>();
}
}
""");
}
}
Loading