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
107 changes: 105 additions & 2 deletions Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,8 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
}

SimpleNameSyntax methodNameSyntax = lambdaMemberAccess.Name;
ArgumentListSyntax transformedArgs = TransformMoqItReferences(lambdaBody.ArgumentList);
ArgumentListSyntax transformedArgs = TransformMoqItReferences(
TransformRefAndOutArguments(lambdaBody.ArgumentList, semanticModel, cancellationToken));

InvocationExpressionSyntax replacement;
if (navigationChain.Count == 0)
Expand Down Expand Up @@ -594,7 +595,8 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
}

SimpleNameSyntax methodNameSyntax = lambdaMemberAccess.Name;
ArgumentListSyntax transformedArgs = TransformMoqItReferences(lambdaBody.ArgumentList);
ArgumentListSyntax transformedArgs = TransformMoqItReferences(
TransformRefAndOutArguments(lambdaBody.ArgumentList, semanticModel, cancellationToken));

InvocationExpressionSyntax baseInvocation;
if (navigationChain.Count == 0)
Expand Down Expand Up @@ -927,6 +929,107 @@ private static bool IsMoqItCall(InvocationExpressionSyntax invocation, out strin
return true;
}

private static ArgumentListSyntax TransformRefAndOutArguments(
ArgumentListSyntax args,
SemanticModel? semanticModel,
CancellationToken cancellationToken)
{
IdentifierNameSyntax itIdentifier = SyntaxFactory.IdentifierName("It");

return args.ReplaceNodes(
args.Arguments.Where(a =>
a.RefKindKeyword.IsKind(SyntaxKind.OutKeyword) ||
a.RefKindKeyword.IsKind(SyntaxKind.RefKeyword)),
(original, _) =>
{
ExpressionSyntax expr = original.Expression.WithoutTrivia();

if (original.RefKindKeyword.IsKind(SyntaxKind.OutKeyword))
{
// out varName → It.IsOut(() => varName)
ParenthesizedLambdaExpressionSyntax lambda = SyntaxFactory.ParenthesizedLambdaExpression(expr);
InvocationExpressionSyntax isOut = BuildItInvocation(
itIdentifier, "IsOut", null,
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList([SyntaxFactory.Argument(lambda),])));
return SyntaxFactory.Argument(isOut).WithTriviaFrom(original);
}

// ref Moq.It.Ref<T>.IsAny → It.IsAnyRef<T>()
if (TryExtractItRefIsAnyTypeArgs(original.Expression, out TypeArgumentListSyntax? isAnyTypeArgs))
{
InvocationExpressionSyntax isAnyRef = BuildItInvocation(
itIdentifier, "IsAnyRef", isAnyTypeArgs,
SyntaxFactory.ArgumentList());
return SyntaxFactory.Argument(isAnyRef).WithTriviaFrom(original);
}

// ref expr → It.IsRef<T>(_ => expr)
TypeArgumentListSyntax? typeArgs = null;
if (semanticModel is not null)
{
ITypeSymbol? typeSymbol = semanticModel.GetTypeInfo(original.Expression, cancellationToken).Type;
if (typeSymbol is not null)
{
TypeSyntax typeSyntax = SyntaxFactory.ParseTypeName(
typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).WithoutTrivia();
typeArgs = SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList([typeSyntax,]));
}
}

SimpleLambdaExpressionSyntax refLambda = SyntaxFactory.SimpleLambdaExpression(
SyntaxFactory.Parameter(SyntaxFactory.Identifier("_")),
expr);
InvocationExpressionSyntax isRef = BuildItInvocation(
itIdentifier, "IsRef", typeArgs,
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList([SyntaxFactory.Argument(refLambda),])));
return SyntaxFactory.Argument(isRef).WithTriviaFrom(original);
});
}

/// <summary>
/// Matches <c>[Moq.]It.Ref&lt;T&gt;.IsAny</c> and returns the type argument list.
/// </summary>
private static bool TryExtractItRefIsAnyTypeArgs(ExpressionSyntax expression,
out TypeArgumentListSyntax? typeArgs)
{
typeArgs = null;
// expression must be: <receiver>.IsAny
if (expression is not MemberAccessExpressionSyntax { Name.Identifier.Text: "IsAny", } isAnyAccess)
{
return false;
}

// receiver must be: It.Ref<T> or Moq.It.Ref<T>
if (isAnyAccess.Expression is not MemberAccessExpressionSyntax refAccess)
{
return false;
}

if (refAccess.Name is not GenericNameSyntax { Identifier.Text: "Ref", TypeArgumentList: var tArgs, })
{
return false;
}

bool isIt = refAccess.Expression switch
{
IdentifierNameSyntax { Identifier.Text: "It", } => true,
MemberAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier.Text: "Moq", },
Name.Identifier.Text: "It",
} => true,
_ => false,
};

if (!isIt)
{
return false;
}

typeArgs = tArgs;
return true;
}

private static InvocationExpressionSyntax BuildItInvocation(
IdentifierNameSyntax itIdentifier,
string methodName,
Expand Down
51 changes: 0 additions & 51 deletions Tests/Mockolate.Migration.Example.Tests/Examples.cs

This file was deleted.

135 changes: 135 additions & 0 deletions Tests/Mockolate.Migration.Example.Tests/MoqMigrationExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System.Text.RegularExpressions;
using Moq;
using Range = Moq.Range;

namespace Mockolate.Migration.Example.Tests;

public class MoqMigrationExamples
{
[Fact]
public async Task ExpectedMigrationResult()
{
IFoo mock = IFoo.CreateMock();
Comment thread
vbreuss marked this conversation as resolved.

mock.Mock.Setup.DoSomething("ping").Returns(true);
Comment thread
vbreuss marked this conversation as resolved.

// out arguments
string outString = "ack";
// TryParse will return true, and the out argument will return "ack", lazy evaluated
mock.Mock.Setup.TryParse("ping", It.IsOut(() => outString)).Returns(true);

// ref arguments
Bar instance = new();
// Only matches if the ref argument to the invocation is the same instance
mock.Mock.Setup.Submit(It.IsRef<Bar>(_ => instance)).Returns(true);

// access invocation arguments when returning a value
mock.Mock.Setup.DoSomethingStringy(It.IsAny<string>())
.Returns(s => s.ToLower());
// Multiple parameters overloads available

// throwing when invoked with specific parameters
mock.Mock.Setup.DoSomething("reset").Throws<InvalidOperationException>();
mock.Mock.Setup.DoSomething("").Throws(new ArgumentException("command"));

// lazy evaluating return value
int count = 1;
mock.Mock.Setup.GetCount().Returns(() => count);


/* ------ Matching Argument ------ */
// any value
mock.Mock.Setup.DoSomething(It.IsAny<string>()).Returns(true);

// any value passed in a `ref` parameter (requires Moq 4.8 or later):
mock.Mock.Setup.Submit(It.IsAnyRef<Bar>()).Returns(true);

// matching Func<int>, lazy evaluated
mock.Mock.Setup.Add(It.Satisfies<int>(i => i % 2 == 0)).Returns(true);

// matching ranges
mock.Mock.Setup.Add(It.IsInRange(0, 10)).Returns(true);

// matching regex
mock.Mock.Setup.DoSomethingStringy(It.Matches("[a-d]+").AsRegex(RegexOptions.IgnoreCase)).Returns("foo");
}

/// <summary>
/// <see href="https://github.com/devlooped/moq/wiki/Quickstart" />
/// </summary>
[Fact]
public async Task MoqCreation()
{
#pragma warning disable MockolateM001
Mock<IFoo> mock = new();
#pragma warning restore MockolateM001

Comment thread
vbreuss marked this conversation as resolved.
mock.Setup(foo => foo.DoSomething("ping")).Returns(true);

// out arguments
string outString = "ack";
// TryParse will return true, and the out argument will return "ack", lazy evaluated
mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true);

// ref arguments
Bar instance = new();
// Only matches if the ref argument to the invocation is the same instance
mock.Setup(foo => foo.Submit(ref instance)).Returns(true);

// access invocation arguments when returning a value
mock.Setup(x => x.DoSomethingStringy(Moq.It.IsAny<string>()))
.Returns((string s) => s.ToLower());
// Multiple parameters overloads available

// throwing when invoked with specific parameters
mock.Setup(foo => foo.DoSomething("reset")).Throws<InvalidOperationException>();
mock.Setup(foo => foo.DoSomething("")).Throws(new ArgumentException("command"));

// lazy evaluating return value
int count = 1;
mock.Setup(foo => foo.GetCount()).Returns(() => count);


/* ------ Matching Argument ------ */
// any value
mock.Setup(foo => foo.DoSomething(Moq.It.IsAny<string>())).Returns(true);

// any value passed in a `ref` parameter (requires Moq 4.8 or later):
mock.Setup(foo => foo.Submit(ref Moq.It.Ref<Bar>.IsAny)).Returns(true);

// matching Func<int>, lazy evaluated
mock.Setup(foo => foo.Add(Moq.It.Is<int>(i => i % 2 == 0))).Returns(true);

// matching ranges
mock.Setup(foo => foo.Add(Moq.It.IsInRange(0, 10, Range.Inclusive))).Returns(true);

// matching regex
mock.Setup(x => x.DoSomethingStringy(Moq.It.IsRegex("[a-d]+", RegexOptions.IgnoreCase))).Returns("foo");
}

public interface IFoo
{
Bar Bar { get; set; }
string Name { get; set; }
int Value { get; set; }
bool DoSomething(string value);
bool DoSomething(int number, string value);
Task<bool> DoSomethingAsync();
string DoSomethingStringy(string value);
bool TryParse(string value, out string outputValue);
bool Submit(ref Bar bar);
int GetCount();
bool Add(int value);
}

public class Bar
{
public virtual Baz Baz { get; set; }

Check warning on line 127 in Tests/Mockolate.Migration.Example.Tests/MoqMigrationExamples.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Non-nullable property 'Baz' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ1d5brKQBrRbJAp-ufj&open=AZ1d5brKQBrRbJAp-ufj&pullRequest=19
public virtual bool Submit() => false;
}

public class Baz
{
public virtual string Name { get; set; }

Check warning on line 133 in Tests/Mockolate.Migration.Example.Tests/MoqMigrationExamples.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ1d5brKQBrRbJAp-ufi&open=AZ1d5brKQBrRbJAp-ufi&pullRequest=19
}
}
Loading
Loading