diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs index 30678f1..8e25c04 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs @@ -390,7 +390,8 @@ private static Dictionary + 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.IsAny → It.IsAnyRef() + 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(_ => 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); + }); + } + + /// + /// Matches [Moq.]It.Ref<T>.IsAny and returns the type argument list. + /// + private static bool TryExtractItRefIsAnyTypeArgs(ExpressionSyntax expression, + out TypeArgumentListSyntax? typeArgs) + { + typeArgs = null; + // expression must be: .IsAny + if (expression is not MemberAccessExpressionSyntax { Name.Identifier.Text: "IsAny", } isAnyAccess) + { + return false; + } + + // receiver must be: It.Ref or Moq.It.Ref + 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, diff --git a/Tests/Mockolate.Migration.Example.Tests/Examples.cs b/Tests/Mockolate.Migration.Example.Tests/Examples.cs deleted file mode 100644 index 8cff3ff..0000000 --- a/Tests/Mockolate.Migration.Example.Tests/Examples.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Mockolate.Verify; -using Moq; - -namespace Mockolate.Migration.Example.Tests; - -public class Examples -{ - [Fact] - public async Task ExpectedMigrationResult() - { - IChocolateDispenser sut = IChocolateDispenser.CreateMock(); - - sut.Mock.Setup.Dispense(It.IsAny(), It.Satisfies(a => a > 0)) - .Do((x, y) => { }) - .Returns(true); - sut.Mock.Setup.Dispense(It.IsAny(), It.Satisfies(a => a < 0)) - .Do(() => { }) - .Throws(); - - sut.Mock.Raise.ChocolateDispensed("foo", 3); - IChocolateDispenser x = sut; - - bool result = x.Dispense("Dark", 1); - sut.Mock.Verify.Dispense(It.Matches("foo").AsRegex(), It.Satisfies(a => a > 2)).Never(); - - await That(result).IsTrue(); - } - - [Fact] - public async Task MoqCreation() - { -#pragma warning disable MockolateM001 - Mock sut = new(); -#pragma warning restore MockolateM001 - - sut.Setup(m => m.Dispense(Moq.It.IsAny(), Moq.It.Is(x => x > 0))) - .Callback((x, y) => { }) - .Returns(true); - sut.Setup(m => m.Dispense(Moq.It.IsAny(), Moq.It.Is(x => x < 0))) - .Callback(() => { }) - .Throws(); - - sut.Raise(m => m.ChocolateDispensed += null, "foo", 3); - IChocolateDispenser x = sut.Object; - - bool result = x.Dispense("Dark", 1); - - sut.Verify(m => m.Dispense(Moq.It.IsRegex("foo"), Moq.It.Is(a => a > 2)), Times.Never); - await That(result).IsTrue(); - } -} diff --git a/Tests/Mockolate.Migration.Example.Tests/MoqMigrationExamples.cs b/Tests/Mockolate.Migration.Example.Tests/MoqMigrationExamples.cs new file mode 100644 index 0000000..75adbc2 --- /dev/null +++ b/Tests/Mockolate.Migration.Example.Tests/MoqMigrationExamples.cs @@ -0,0 +1,139 @@ +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(); + + mock.Mock.Setup.DoSomething("ping").Returns(true); + + // 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(_ => instance)).Returns(true); + + // access invocation arguments when returning a value + mock.Mock.Setup.DoSomethingStringy(It.IsAny()) + .Returns(s => s.ToLower()); + // Multiple parameters overloads available + + // throwing when invoked with specific parameters + mock.Mock.Setup.DoSomething("reset").Throws(); + 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()).Returns(true); + + // any value passed in a `ref` parameter (requires Moq 4.8 or later): + mock.Mock.Setup.Submit(It.IsAnyRef()).Returns(true); + + // matching Func, lazy evaluated + mock.Mock.Setup.Add(It.Satisfies(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"); + + await That(true).IsTrue(); + } + + /// + /// + /// + [Fact] + public async Task MoqCreation() + { +#pragma warning disable MockolateM001 + Mock mock = new(); +#pragma warning restore MockolateM001 + + 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())) + .Returns((string s) => s.ToLower()); + // Multiple parameters overloads available + + // throwing when invoked with specific parameters + mock.Setup(foo => foo.DoSomething("reset")).Throws(); + 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())).Returns(true); + + // any value passed in a `ref` parameter (requires Moq 4.8 or later): + mock.Setup(foo => foo.Submit(ref Moq.It.Ref.IsAny)).Returns(true); + + // matching Func, lazy evaluated + mock.Setup(foo => foo.Add(Moq.It.Is(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"); + + await That(true).IsTrue(); + } + + 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 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; } + public virtual bool Submit() => false; + } + + public class Baz + { + public virtual string Name { get; set; } = ""; + } +} diff --git a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs index 3f97229..05b1d32 100644 --- a/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs +++ b/Tests/Mockolate.Migration.Tests/MoqCodeFixProviderTests.SetupTests.cs @@ -212,5 +212,116 @@ public void Test() } } """); + + [Fact] + public async Task Method_WithOutParameter_MigratedToIsOut() + => await Verifier.VerifyCodeFixAsync( + """ + using Moq; + + public interface IFoo { bool TryParse(string value, out string outputValue); } + + public class Tests + { + public void Test() + { + var mock = [|new Mock()|]; + string outString = "ack"; + mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true); + } + } + """, + """ + using Moq; + using Mockolate; + + public interface IFoo { bool TryParse(string value, out string outputValue); } + + public class Tests + { + public void Test() + { + var mock = IFoo.CreateMock(); + string outString = "ack"; + mock.Mock.Setup.TryParse("ping", It.IsOut(() => outString)).Returns(true); + } + } + """); + + [Fact] + public async Task Method_WithRefItRefIsAny_MigratedToIsAnyRef() + => await Verifier.VerifyCodeFixAsync( + """ + using Moq; + + public class Bar { } + + public interface IFoo { bool Submit(ref Bar bar); } + + public class Tests + { + public void Test() + { + var mock = [|new Mock()|]; + mock.Setup(foo => foo.Submit(ref Moq.It.Ref.IsAny)).Returns(true); + } + } + """, + """ + using Moq; + using Mockolate; + + public class Bar { } + + public interface IFoo { bool Submit(ref Bar bar); } + + public class Tests + { + public void Test() + { + var mock = IFoo.CreateMock(); + mock.Mock.Setup.Submit(It.IsAnyRef()).Returns(true); + } + } + """); + + [Fact] + public async Task Method_WithRefParameter_MigratedToIsRef() + => await Verifier.VerifyCodeFixAsync( + """ + using Moq; + + public class Bar { } + + public interface IFoo { bool Submit(ref Bar bar); } + + public class Tests + { + public void Test() + { + var mock = [|new Mock()|]; + Bar instance = new(); + mock.Setup(foo => foo.Submit(ref instance)).Returns(true); + } + } + """, + """ + using Moq; + using Mockolate; + + public class Bar { } + + public interface IFoo { bool Submit(ref Bar bar); } + + public class Tests + { + public void Test() + { + var mock = IFoo.CreateMock(); + Bar instance = new(); + mock.Mock.Setup.Submit(It.IsRef(_ => instance)).Returns(true); + } + } + """); } }