nunit.analyzers icon indicating copy to clipboard operation
nunit.analyzers copied to clipboard

Replace UpdateStringFormatToFormattableString with String.Format

Open Bartleby2718 opened this issue 10 months ago • 5 comments

Consider the following (mostly similar) tests:

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInStandardOrder1()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            ↓ClassicAssert.AreEqual(""expected"", ""actual"", ""{0}"", ""first"");
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            Assert.That(""actual"", Is.EqualTo(""expected""), $""{""first""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInStandardOrder2()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            ↓ClassicAssert.AreEqual(""expected"", ""actual"", ""{0}"", new[] { ""first"" });
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            Assert.That(""actual"", Is.EqualTo(""expected""), $""{""first""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInStandardOrder3()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            var args = new[] { ""first"", ""second"" };
            ↓ClassicAssert.AreEqual(""expected"", ""actual"", ""{0}, {1}"", args);
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            var args = new[] { ""first"", ""second"" };
            Assert.That(""actual"", Is.EqualTo(""expected""), $""{""first""}, {""second""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInStandardOrder4()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            ↓ClassicAssert.AreEqual(""expected"", ""actual"", ""{0}, {1}"", ""first"", ""second"");
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            Assert.That(""actual"", Is.EqualTo(""expected""), $""{""first""}, {""second""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInStandardOrder5()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            ↓ClassicAssert.AreEqual(""expected"", ""actual"", ""{0}, {1}"", new[] { ""first"", ""second"" });
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            Assert.That(""actual"", Is.EqualTo(""expected""), $""{""first""}, {""second""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInNonstandardOrder1()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            ↓ClassicAssert.AreEqual(args: ""first"", actual: ""actual"", message: ""{0}"", expected: ""expected"");
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            Assert.That(actual: ""actual"", Is.EqualTo(expected: ""expected""), $""{""first""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInNonstandardOrder2()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            ↓ClassicAssert.AreEqual(args: new[] { ""first"" }, actual: ""actual"", message: ""{0}"", expected: ""expected"");
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            Assert.That(actual: ""actual"", Is.EqualTo(expected: ""expected""), $""{""first""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInNonstandardOrder3()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            ↓ClassicAssert.AreEqual(args: new[] { ""first"", ""second"" }, actual: ""actual"", message: ""{0}, {1}"", expected: ""expected"");
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            Assert.That(actual: ""actual"", Is.EqualTo(expected: ""expected""), $""{""first""}, {""second""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

        [Test]
        public void VerifyAreEqualFixWithMessageAndParamsInNonstandardOrder4()
        {
            var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            ↓ClassicAssert.AreEqual(args: new[] { ""first"", ""second"", ""third"" }, actual: ""actual"", message: ""{0}, {1}, {2}"", expected: ""expected"");
        }");
            var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
        public void TestMethod()
        {
            Assert.That(actual: ""actual"", Is.EqualTo(expected: ""expected""), $""{""first""}, {""second""}, {""third""}"");
        }");
            RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
        }

Some of these fail, even after #716. In order to fix InStandardOrder3, in particular, I believe we should replace GetInterpolatedMessageArgumentOrDefault with String.Format:

            var stringFormat = SyntaxFactory.MemberAccessExpression(
                SyntaxKind.SimpleMemberAccessExpression,
                SyntaxFactory.IdentifierName("String"),
                SyntaxFactory.IdentifierName("Format"));

            var argumentList = SyntaxFactory.ArgumentList(
                SyntaxFactory.SeparatedList<ArgumentSyntax>(
                    new SyntaxNodeOrToken[] { messageArgument.WithNameColon(null) }.Concat(
                        args.SelectMany((arg, _) => new SyntaxNodeOrToken[] { SyntaxFactory.Token(SyntaxKind.CommaToken), arg.WithNameColon(null) }))));

            return SyntaxFactory.Argument(SyntaxFactory.InvocationExpression(stringFormat, argumentList));

I think this will be come truly startable once https://github.com/dotnet/roslyn/issues/68469 is done and released.

Bartleby2718 avatar Apr 07 '24 19:04 Bartleby2718

@Bartleby2718 The difference between an InterpolatedString and String.Format is that the first is lazy, i.e. the string is only formatted when the test fails. The latter will always be formatted before the test is run, making the test call slower.

For completeness your tests are valid and in case a NUnit test actually uses an args array, instead of separate arguments, we cannot even convert it if the args array is not-created in-line but in a separate statement.

So yes we would need to convert:

object[] args = new[] { ""first"", ""second"" };
ClassicAssert.AreEqual(args: args, actual: ""actual"", message: ""{0}, {1}"", expected: ""expected"");

into:

object[] args = new[] { ""first"", ""second"" };
Assert.That(actual: ""actual"", Is.EqualTo(expected: ""expected""), () => string.Format(""{0}, {1}"", args);

Note the addition of () => to make the call lazy.

manfred-brands avatar Apr 08 '24 02:04 manfred-brands

This issue is not just limited to 'named:' arguments, the current code will fail if the parameters for the format string are in an array.

[TestCase(3.0, 4)]
public void Test(object actual, object expected)
{
    object[] args = { expected, actual };
    Assert.AreEqual(expected, actual, "Expected: {0} Got: {1}", args);
}

The AreEqual gets converted into:

Assert.AreEqual(expected, actual, "Expected: {0} Got: {1}", args);

But should be converted into:

Assert.That(actual, Is.EqualTo(expected), () => string.Format("Expected: {0} Got: {1}", args));

manfred-brands avatar Apr 10 '24 03:04 manfred-brands

Similar the following fails:

ClassicAssert.AreEqual(expected, actual, GetLocalizedFormatSpecification(), expected, actual);

manfred-brands avatar Apr 10 '24 03:04 manfred-brands

I have implemented this on top of #716 and will rebase when that is merged.

manfred-brands avatar Apr 10 '24 10:04 manfred-brands