diff --git a/src/FluentAssertions.Analyzers.Tests/DiagnosticVerifier.cs b/src/FluentAssertions.Analyzers.Tests/DiagnosticVerifier.cs index 73b9bbc7..1fff12ad 100644 --- a/src/FluentAssertions.Analyzers.Tests/DiagnosticVerifier.cs +++ b/src/FluentAssertions.Analyzers.Tests/DiagnosticVerifier.cs @@ -34,7 +34,10 @@ static DiagnosticVerifier() typeof(Compilation), // Microsoft.CodeAnalysis typeof(AssertionScope), // FluentAssertions.Core typeof(AssertionExtensions), // FluentAssertions + typeof(Microsoft.VisualStudio.TestTools.UnitTesting.Assert) // MsTest }.Select(type => type.GetTypeInfo().Assembly.Location) + .Append(GetSystemAssemblyPathByName("System.Globalization.dll")) + .Append(GetSystemAssemblyPathByName("System.Text.RegularExpressions.dll")) .Append(GetSystemAssemblyPathByName("System.Runtime.Extensions.dll")) .Append(GetSystemAssemblyPathByName("System.Data.Common.dll")) .Append(GetSystemAssemblyPathByName("System.Threading.Tasks.dll")) @@ -641,7 +644,7 @@ private static string FormatDiagnostics(DiagnosticAnalyzer[] analyzers, params D private static DiagnosticAnalyzer[] CreateAllAnalyzers() { - var assembly = typeof(Constants).Assembly; + var assembly = typeof(Constants).Assembly; var analyzersTypes = assembly.GetTypes() .Where(type => !type.IsAbstract && typeof(DiagnosticAnalyzer).IsAssignableFrom(type)); var analyzers = analyzersTypes.Select(type => (DiagnosticAnalyzer)Activator.CreateInstance(type)); diff --git a/src/FluentAssertions.Analyzers.Tests/GenerateCode.cs b/src/FluentAssertions.Analyzers.Tests/GenerateCode.cs index 841af468..f00c1403 100644 --- a/src/FluentAssertions.Analyzers.Tests/GenerateCode.cs +++ b/src/FluentAssertions.Analyzers.Tests/GenerateCode.cs @@ -175,6 +175,25 @@ public static string GenericIListExpressionBodyAssertion(string assertion) => Ge .AppendLine("}") .ToString(); + public static string MsTestAssertion(string methodArguments, string assertion) => new StringBuilder() + .AppendLine("using System;") + .AppendLine("using FluentAssertions;") + .AppendLine("using FluentAssertions.Extensions;") + .AppendLine("using Microsoft.VisualStudio.TestTools.UnitTesting;") + .AppendLine("using System.Threading.Tasks;") + .AppendLine("namespace TestNamespace") + .AppendLine("{") + .AppendLine(" class TestClass") + .AppendLine(" {") + .AppendLine($" void TestMethod({methodArguments})") + .AppendLine(" {") + .AppendLine($" {assertion}") + .AppendLine(" }") + .AppendLine(" }") + .AppendMainMethod() + .AppendLine("}") + .ToString(); + private static StringBuilder AppendMainMethod(this StringBuilder builder) => builder .AppendLine(" class Program") .AppendLine(" {") diff --git a/src/FluentAssertions.Analyzers.Tests/Tips/MsTestTests.cs b/src/FluentAssertions.Analyzers.Tests/Tips/MsTestTests.cs new file mode 100644 index 00000000..79798501 --- /dev/null +++ b/src/FluentAssertions.Analyzers.Tests/Tips/MsTestTests.cs @@ -0,0 +1,545 @@ +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace FluentAssertions.Analyzers.Tests.Tips +{ + [TestClass] + public class MsTestTests + { + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.IsTrue(actual{0});")] + [AssertionDiagnostic("Assert.IsTrue(bool.Parse(\"true\"){0});")] + [Implemented] + public void AssertIsTrue_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("bool actual", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.IsTrue(actual{0});", + newAssertion: "actual.Should().BeTrue({0});")] + [AssertionCodeFix( + oldAssertion: "Assert.IsTrue(bool.Parse(\"true\"){0});", + newAssertion: "bool.Parse(\"true\").Should().BeTrue({0});")] + [Implemented] + public void AssertIsTrue_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("bool actual", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.IsFalse(actual{0});")] + [AssertionDiagnostic("Assert.IsFalse(bool.Parse(\"true\"){0});")] + [Implemented] + public void AssertIsFalse_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("bool actual", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.IsFalse(actual{0});", + newAssertion: "actual.Should().BeFalse({0});")] + [AssertionCodeFix( + oldAssertion: "Assert.IsFalse(bool.Parse(\"true\"){0});", + newAssertion: "bool.Parse(\"true\").Should().BeFalse({0});")] + [Implemented] + public void AssertIsFalse_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("bool actual", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.IsNull(actual{0});")] + [Implemented] + public void AssertIsNull_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("object actual", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.IsNull(actual{0});", + newAssertion: "actual.Should().BeNull({0});")] + [Implemented] + public void AssertIsNull_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("object actual", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.IsNotNull(actual{0});")] + [Implemented] + public void AssertIsNotNull_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("object actual", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.IsNotNull(actual{0});", + newAssertion: "actual.Should().NotBeNull({0});")] + [Implemented] + public void AssertIsNotNull_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("object actual", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.IsInstanceOfType(actual, type{0});")] + [AssertionDiagnostic("Assert.IsInstanceOfType(actual, typeof(string){0});")] + [Implemented] + public void AssertIsInstanceOfType_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("object actual, Type type", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.IsInstanceOfType(actual, type{0});", + newAssertion: "actual.Should().BeOfType(type{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.IsInstanceOfType(actual, typeof(string){0});", + newAssertion: "actual.Should().BeOfType({0});")] + [Implemented] + public void AssertIsInstanceOfType_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("object actual, Type type", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.IsNotInstanceOfType(actual, type{0});")] + [AssertionDiagnostic("Assert.IsNotInstanceOfType(actual, typeof(string){0});")] + [Implemented] + public void AssertIsNotInstanceOfType_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("object actual, Type type", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.IsNotInstanceOfType(actual, type{0});", + newAssertion: "actual.Should().NotBeOfType(type{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.IsNotInstanceOfType(actual, typeof(string){0});", + newAssertion: "actual.Should().NotBeOfType({0});")] + [Implemented] + public void AssertIsNotInstanceOfType_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("object actual, Type type", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreEqual(expected, actual{0});")] + [Implemented] + public void AssertObjectAreEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("object actual, object expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual{0});", + newAssertion: "actual.Should().Be(expected{0});")] + [Implemented] + public void AssertObjectAreEqual_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("object actual, object expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreEqual(expected, actual, delta{0});")] + [AssertionDiagnostic("Assert.AreEqual(expected, actual, 0.6{0});")] + [Implemented] + public void AssertDoubleAreEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("double actual, double expected, double delta", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual, delta{0});", + newAssertion: "actual.Should().BeApproximately(expected, delta{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual, 0.6{0});", + newAssertion: "actual.Should().BeApproximately(expected, 0.6{0});")] + [Implemented] + public void AssertDoubleAreEqual_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("double actual, double expected, double delta", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreEqual(expected, actual, delta{0});")] + [AssertionDiagnostic("Assert.AreEqual(expected, actual, 0.6f{0});")] + [Implemented] + public void AssertFloatAreEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("float actual, float expected, float delta", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual, delta{0});", + newAssertion: "actual.Should().BeApproximately(expected, delta{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual, 0.6f{0});", + newAssertion: "actual.Should().BeApproximately(expected, 0.6f{0});")] + [Implemented] + public void AssertFloatAreEqual_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("float actual, float expected, float delta", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreEqual(expected, actual{0});")] + [AssertionDiagnostic("Assert.AreEqual(expected, actual, false{0});")] + [AssertionDiagnostic("Assert.AreEqual(expected, actual, true{0});")] + [AssertionDiagnostic("Assert.AreEqual(expected, actual, false, System.Globalization.CultureInfo.CurrentCulture{0});")] + [AssertionDiagnostic("Assert.AreEqual(expected, actual, true, System.Globalization.CultureInfo.CurrentCulture{0});")] + [Implemented] + public void AssertStringAreEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("string actual, string expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual{0});", + newAssertion: "actual.Should().Be(expected{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual, false{0});", + newAssertion: "actual.Should().Be(expected{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual, true{0});", + newAssertion: "actual.Should().BeEquivalentTo(expected{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual, false, System.Globalization.CultureInfo.CurrentCulture{0});", + newAssertion: "actual.Should().Be(expected{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreEqual(expected, actual, true, System.Globalization.CultureInfo.CurrentCulture{0});", + newAssertion: "actual.Should().BeEquivalentTo(expected{0});")] + [Implemented] + public void AssertStringAreEqual_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("string actual, string expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual{0});")] + [Implemented] + public void AssertObjectAreNotEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("object actual, object expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual{0});", + newAssertion: "actual.Should().NotBe(expected{0});")] + [Implemented] + public void AssertObjectAreNotEqual_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("object actual, object expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual, delta{0});")] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual, 0.6{0});")] + [Implemented] + public void AssertDoubleAreNotEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("double actual, double expected, double delta", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual, delta{0});", + newAssertion: "actual.Should().NotBeApproximately(expected, delta{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual, 0.6{0});", + newAssertion: "actual.Should().NotBeApproximately(expected, 0.6{0});")] + [Implemented] + public void AssertDoubleAreNotEqual_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("double actual, double expected, double delta", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual, delta{0});")] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual, 0.6f{0});")] + [Implemented] + public void AssertFloatAreNotEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("float actual, float expected, float delta", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual, delta{0});", + newAssertion: "actual.Should().NotBeApproximately(expected, delta{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual, 0.6f{0});", + newAssertion: "actual.Should().NotBeApproximately(expected, 0.6f{0});")] + [Implemented] + public void AssertFloatAreNotEqual_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("float actual, float expected, float delta", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual{0});")] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual, false{0});")] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual, true{0});")] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual, false, System.Globalization.CultureInfo.CurrentCulture{0});")] + [AssertionDiagnostic("Assert.AreNotEqual(expected, actual, true, System.Globalization.CultureInfo.CurrentCulture{0});")] + [Implemented] + public void AssertStringAreNotEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("string actual, string expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual{0});", + newAssertion: "actual.Should().NotBe(expected{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual, false{0});", + newAssertion: "actual.Should().NotBe(expected{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual, true{0});", + newAssertion: "actual.Should().NotBeEquivalentTo(expected{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual, false, System.Globalization.CultureInfo.CurrentCulture{0});", + newAssertion: "actual.Should().NotBe(expected{0});")] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotEqual(expected, actual, true, System.Globalization.CultureInfo.CurrentCulture{0});", + newAssertion: "actual.Should().NotBeEquivalentTo(expected{0});")] + [Implemented] + public void AssertStringAreNotEqual_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("string actual, string expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreSame(expected, actual{0});")] + [Implemented] + public void AssertAreSame_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("object actual, object expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreSame(expected, actual{0});", + newAssertion: "actual.Should().BeSameAs(expected{0});")] + [Implemented] + public void AssertAreSame_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("object actual, object expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.AreNotSame(expected, actual{0});")] + [Implemented] + public void AssertAreNotSame_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("object actual, object expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.AreNotSame(expected, actual{0});", + newAssertion: "actual.Should().NotBeSameAs(expected{0});")] + [Implemented] + public void AssertAreNotSame_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("object actual, object expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.ThrowsException(action{0});")] + [Implemented] + public void AssertThrowsException_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("Action action", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.ThrowsException(action{0});", + newAssertion: "action.Should().ThrowExactly({0});")] + [Implemented] + public void AssertThrowsException_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("Action action", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("Assert.ThrowsExceptionAsync(action{0});")] + [Implemented] + public void AssertThrowsExceptionAsync_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("Func action", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "Assert.ThrowsExceptionAsync(action{0});", + newAssertion: "action.Should().ThrowExactlyAsync({0});")] + [Implemented] + public void AssertThrowsExceptionAsync_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("Func action", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.AllItemsAreInstancesOfType(actual, type{0});")] + [AssertionDiagnostic("CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(int){0});")] + [AssertionDiagnostic("CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(Int32){0});")] + [AssertionDiagnostic("CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(System.Int32){0});")] + [Implemented] + public void CollectionAssertAllItemsAreInstancesOfType_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, Type type", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AllItemsAreInstancesOfType(actual, type{0});", + newAssertion: "actual.Should().AllBeOfType(type{0});")] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(int){0});", + newAssertion: "actual.Should().AllBeOfType({0});")] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(Int32){0});", + newAssertion: "actual.Should().AllBeOfType({0});")] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(System.Int32){0});", + newAssertion: "actual.Should().AllBeOfType({0});")] + [Implemented] + public void CollectionAssertAllItemsAreInstancesOfType_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, Type type", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.AreEqual(expected, actual{0});")] + [Implemented] + public void CollectionAssertAreEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, System.Collections.Generic.List expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AreEqual(expected, actual{0});", + newAssertion: "actual.Should().Equal(expected{0});")] + [Implemented] + public void CollectionAssertAreEqual_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, System.Collections.Generic.List expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.AreNotEqual(expected, actual{0});")] + [Implemented] + public void CollectionAssertAreNotEqual_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, System.Collections.Generic.List expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AreNotEqual(expected, actual{0});", + newAssertion: "actual.Should().NotEqual(expected{0});")] + [Implemented] + public void CollectionAssertAreNotEqual_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, System.Collections.Generic.List expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.AreEquivalent(expected, actual{0});")] + [Implemented] + public void CollectionAssertAreEquivalent_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, System.Collections.Generic.List expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AreEquivalent(expected, actual{0});", + newAssertion: "actual.Should().BeEquivalentTo(expected{0});")] + [Implemented] + public void CollectionAssertAreEquivalent_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, System.Collections.Generic.List expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.AreNotEquivalent(expected, actual{0});")] + [Implemented] + public void CollectionAssertAreNotEquivalent_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, System.Collections.Generic.List expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AreNotEquivalent(expected, actual{0});", + newAssertion: "actual.Should().NotBeEquivalentTo(expected{0});")] + [Implemented] + public void CollectionAssertAreNotEquivalent_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, System.Collections.Generic.List expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.AllItemsAreNotNull(actual{0});")] + [Implemented] + public void CollectionAssertAllItemsAreNotNull_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AllItemsAreNotNull(actual{0});", + newAssertion: "actual.Should().NotContainNulls({0});")] + [Implemented] + public void CollectionAssertAllItemsAreNotNull_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.AllItemsAreUnique(actual{0});")] + [Implemented] + public void CollectionAssertAllItemsAreUnique_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.AllItemsAreUnique(actual{0});", + newAssertion: "actual.Should().OnlyHaveUniqueItems({0});")] + [Implemented] + public void CollectionAssertAllItemsAreUnique_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.Contains(actual, expected{0});")] + [Implemented] + public void CollectionAssertContains_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, int expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.Contains(actual, expected{0});", + newAssertion: "actual.Should().Contain(expected{0});")] + [Implemented] + public void CollectionAssertContains_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, int expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.DoesNotContain(actual, expected{0});")] + [Implemented] + public void CollectionAssertDoesNotContain_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, int expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.DoesNotContain(actual, expected{0});", + newAssertion: "actual.Should().NotContain(expected{0});")] + [Implemented] + public void CollectionAssertDoesNotContain_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, int expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.IsSubsetOf(expected, actual{0});")] + [Implemented] + public void CollectionAssertIsSubsetOf_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, System.Collections.Generic.List expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.IsSubsetOf(expected, actual{0});", + newAssertion: "actual.Should().BeSubsetOf(expected{0});")] + [Implemented] + public void CollectionAssertIsSubsetOf_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, System.Collections.Generic.List expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("CollectionAssert.IsNotSubsetOf(expected, actual{0});")] + [Implemented] + public void CollectionAssertIsNotSubsetOf_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("System.Collections.Generic.List actual, System.Collections.Generic.List expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "CollectionAssert.IsNotSubsetOf(expected, actual{0});", + newAssertion: "actual.Should().NotBeSubsetOf(expected{0});")] + [Implemented] + public void CollectionAssertIsNotSubsetOf_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("System.Collections.Generic.List actual, System.Collections.Generic.List expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("StringAssert.Contains(expected, actual{0});")] + [Implemented] + public void StringAssertContains_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("string actual, string expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "StringAssert.Contains(expected, actual{0});", + newAssertion: "actual.Should().Contain(expected{0});")] + [Implemented] + public void StringAssertContains_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("string actual, string expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("StringAssert.StartsWith(expected, actual{0});")] + [Implemented] + public void StringAssertStartsWith_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("string actual, string expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "StringAssert.StartsWith(expected, actual{0});", + newAssertion: "actual.Should().StartWith(expected{0});")] + [Implemented] + public void StringAssertStartsWith_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("string actual, string expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("StringAssert.EndsWith(expected, actual{0});")] + [Implemented] + public void StringAssertEndsWith_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("string actual, string expected", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "StringAssert.EndsWith(expected, actual{0});", + newAssertion: "actual.Should().EndWith(expected{0});")] + [Implemented] + public void StringAssertEndsWith_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("string actual, string expected", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("StringAssert.Matches(actual, pattern{0});")] + [Implemented] + public void StringAssertMatches_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("string actual, System.Text.RegularExpressions.Regex pattern", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "StringAssert.Matches(actual, pattern{0});", + newAssertion: "actual.Should().MatchRegex(pattern{0});")] + [Implemented] + public void StringAssertMatches_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("string actual, System.Text.RegularExpressions.Regex pattern", oldAssertion, newAssertion); + + [AssertionDataTestMethod] + [AssertionDiagnostic("StringAssert.DoesNotMatch(actual, pattern{0});")] + [Implemented] + public void StringAssertDoesNotMatch_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic("string actual, System.Text.RegularExpressions.Regex pattern", assertion); + + [AssertionDataTestMethod] + [AssertionCodeFix( + oldAssertion: "StringAssert.DoesNotMatch(actual, pattern{0});", + newAssertion: "actual.Should().NotMatchRegex(pattern{0});")] + [Implemented] + public void StringAssertDoesNotMatch_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("string actual, System.Text.RegularExpressions.Regex pattern", oldAssertion, newAssertion); + + private void VerifyCSharpDiagnostic(string methodArguments, string assertion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new() + { + var source = GenerateCode.MsTestAssertion(methodArguments, assertion); + + var type = typeof(TDiagnosticAnalyzer); + var diagnosticId = (string)type.GetField("DiagnosticId").GetValue(null); + var message = (string)type.GetField("Message").GetValue(null); + + DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult + { + Id = diagnosticId, + Message = message, + Locations = new DiagnosticResultLocation[] + { + new DiagnosticResultLocation("Test0.cs", 12, 13) + }, + Severity = DiagnosticSeverity.Info + }); + } + + private void VerifyCSharpFix(string methodArguments, string oldAssertion, string newAssertion) + where TCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider, new() + where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new() + { + var oldSource = GenerateCode.MsTestAssertion(methodArguments, oldAssertion); + var newSource = GenerateCode.MsTestAssertion(methodArguments, newAssertion); + + DiagnosticVerifier.VerifyCSharpFix(oldSource, newSource); + } + } +} diff --git a/src/FluentAssertions.Analyzers/Constants.cs b/src/FluentAssertions.Analyzers/Constants.cs index 315505ad..b0518324 100644 --- a/src/FluentAssertions.Analyzers/Constants.cs +++ b/src/FluentAssertions.Analyzers/Constants.cs @@ -83,6 +83,38 @@ public static class Exceptions public const string ExceptionShouldThrowWithMessage = nameof(ExceptionShouldThrowWithMessage); public const string ExceptionShouldThrowWithInnerException = nameof(ExceptionShouldThrowWithInnerException); } + + public static class MsTest + { + public const string AssertIsTrue = nameof(AssertIsTrue); + public const string AssertIsFalse = nameof(AssertIsFalse); + public const string AssertIsNotNull = nameof(AssertIsNotNull); + public const string AssertIsNull = nameof(AssertIsNull); + public const string AssertIsInstanceOfType = nameof(AssertIsInstanceOfType); + public const string AssertIsNotInstanceOfType = nameof(AssertIsNotInstanceOfType); + public const string AssertAreEqual = nameof(AssertAreEqual); + public const string AssertAreNotEqual = nameof(AssertAreNotEqual); + public const string AssertAreSame = nameof(AssertAreSame); + public const string AssertAreNotSame = nameof(AssertAreNotSame); + public const string AssertThrowsException = nameof(AssertThrowsException); + public const string AssertThrowsExceptionAsync = nameof(AssertThrowsExceptionAsync); + public const string StringAssertContains = nameof(StringAssertContains); + public const string StringAssertStartsWith = nameof(StringAssertStartsWith); + public const string StringAssertEndsWith = nameof(StringAssertEndsWith); + public const string StringAssertMatches = nameof(StringAssertMatches); + public const string StringAssertDoesNotMatch = nameof(StringAssertDoesNotMatch); + public const string CollectionAssertAllItemsAreInstancesOfType = nameof(CollectionAssertAllItemsAreInstancesOfType); + public const string CollectionAssertAreEqual = nameof(CollectionAssertAreEqual); + public const string CollectionAssertAreNotEqual = nameof(CollectionAssertAreNotEqual); + public const string CollectionAssertAreEquivalent = nameof(CollectionAssertAreEquivalent); + public const string CollectionAssertAreNotEquivalent = nameof(CollectionAssertAreNotEquivalent); + public const string CollectionAssertAllItemsAreNotNull = nameof(CollectionAssertAllItemsAreNotNull); + public const string CollectionAssertAllItemsAreUnique = nameof(CollectionAssertAllItemsAreUnique); + public const string CollectionAssertContains = nameof(CollectionAssertContains); + public const string CollectionAssertDoesNotContain = nameof(CollectionAssertDoesNotContain); + public const string CollectionAssertIsSubsetOf = nameof(CollectionAssertIsSubsetOf); + public const string CollectionAssertIsNotSubsetOf = nameof(CollectionAssertIsNotSubsetOf); + } } public static class CodeSmell diff --git a/src/FluentAssertions.Analyzers/FluentAssertions.Analyzers.csproj b/src/FluentAssertions.Analyzers/FluentAssertions.Analyzers.csproj index 1b3c919f..98b36005 100644 --- a/src/FluentAssertions.Analyzers/FluentAssertions.Analyzers.csproj +++ b/src/FluentAssertions.Analyzers/FluentAssertions.Analyzers.csproj @@ -9,6 +9,7 @@ FluentAssertions.Analyzers + 8.0 diff --git a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldBeEmpty.cs b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldBeEmpty.cs index 09f4583c..afe9aa52 100644 --- a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldBeEmpty.cs +++ b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldBeEmpty.cs @@ -40,7 +40,7 @@ public class ShouldHaveCount0SyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor { } - private static bool HaveCountArgumentsValidator(SeparatedSyntaxList arguments) + private static bool HaveCountArgumentsValidator(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; diff --git a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldContainSingle.cs b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldContainSingle.cs index 5acbe1ca..c140aac0 100644 --- a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldContainSingle.cs +++ b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldContainSingle.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.Linq; namespace FluentAssertions.Analyzers { diff --git a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldEqualOtherCollectionByComparer.cs b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldEqualOtherCollectionByComparer.cs index c7c72465..6d5a9f6e 100644 --- a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldEqualOtherCollectionByComparer.cs +++ b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldEqualOtherCollectionByComparer.cs @@ -34,12 +34,12 @@ public SelectShouldEqualOtherCollectionSelectSyntaxVisitor() { } - private static bool MathodContainingArgumentInvokingLambda(SeparatedSyntaxList arguments) + private static bool MathodContainingArgumentInvokingLambda(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; return arguments[0].Expression is InvocationExpressionSyntax invocation - && MemberValidator.MethodContainingLambdaPredicate(invocation.ArgumentList.Arguments); + && MemberValidator.MethodContainingLambdaPredicate(invocation.ArgumentList.Arguments, semanticModel); } } } diff --git a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveElementAt.cs b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveElementAt.cs index 756277ef..fcc21cd8 100644 --- a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveElementAt.cs +++ b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveElementAt.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.Linq; namespace FluentAssertions.Analyzers { diff --git a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveSameCount.cs b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveSameCount.cs index dc74f69c..4cfb79ba 100644 --- a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveSameCount.cs +++ b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveSameCount.cs @@ -32,7 +32,7 @@ public class ShouldHaveCountOtherCollectionCountSyntaxVisitor : FluentAssertions { } - private static bool HasArgumentInvokingCountMethod(SeparatedSyntaxList arguments) + private static bool HasArgumentInvokingCountMethod(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; diff --git a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldNotHaveSameCount.cs b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldNotHaveSameCount.cs index 4cacd9d5..91662fb7 100644 --- a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldNotHaveSameCount.cs +++ b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldNotHaveSameCount.cs @@ -32,7 +32,7 @@ public class CountShouldNotBeOtherCollectionCountSyntaxVisitor : FluentAssertion { } - private static bool HasArgumentInvokingCountMethod(SeparatedSyntaxList arguments) + private static bool HasArgumentInvokingCountMethod(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; diff --git a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldOnlyHaveUniqueItems.cs b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldOnlyHaveUniqueItems.cs index 9b1bee0e..cef2764b 100644 --- a/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldOnlyHaveUniqueItems.cs +++ b/src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldOnlyHaveUniqueItems.cs @@ -33,7 +33,7 @@ public class ShouldHaveSameCountThisCollectionDistinctSyntaxVisitor : FluentAsse { } - private static bool ArgumentInvokesDistinctMethod(SeparatedSyntaxList arguments) + private static bool ArgumentInvokesDistinctMethod(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; diff --git a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainPair.cs b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainPair.cs index 7a00e5a2..9655b132 100644 --- a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainPair.cs +++ b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainPair.cs @@ -59,7 +59,7 @@ public override bool IsValid(ExpressionSyntax expression) && keyIdentifier.Identifier.Text == valueIdentifier.Identifier.Text; } - protected static bool KeyIsProperty(SeparatedSyntaxList arguments) + protected static bool KeyIsProperty(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; @@ -67,7 +67,7 @@ protected static bool KeyIsProperty(SeparatedSyntaxList argument && valueAccess.Expression is IdentifierNameSyntax identifier && valueAccess.Name.Identifier.Text == "Key"; } - protected static bool ValueIsProperty(SeparatedSyntaxList arguments) + protected static bool ValueIsProperty(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; diff --git a/src/FluentAssertions.Analyzers/Tips/Exceptions/ExceptionShouldThrowWithInnerException.cs b/src/FluentAssertions.Analyzers/Tips/Exceptions/ExceptionShouldThrowWithInnerException.cs index 360c2c02..b3c56d29 100644 --- a/src/FluentAssertions.Analyzers/Tips/Exceptions/ExceptionShouldThrowWithInnerException.cs +++ b/src/FluentAssertions.Analyzers/Tips/Exceptions/ExceptionShouldThrowWithInnerException.cs @@ -5,7 +5,6 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace FluentAssertions.Analyzers { diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreEqual.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreEqual.cs new file mode 100644 index 00000000..afb5e631 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreEqual.cs @@ -0,0 +1,104 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using TypeSelector = FluentAssertions.Analyzers.Utilities.SemanticModelTypeExtensions; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertAreEqualAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertAreEqual; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeApproximately() for complex numbers and .Should().Be() for other cases."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertFloatAreEqualWithDeltaSyntaxVisitor(); + yield return new AssertDoubleAreEqualWithDeltaSyntaxVisitor(); + yield return new AssertStringAreEqualSyntaxVisitor(); + yield return new AssertObjectAreEqualSyntaxVisitor(); + } + } + + // public static void AreEqual(float expected, float actual, float delta) + public class AssertFloatAreEqualWithDeltaSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertFloatAreEqualWithDeltaSyntaxVisitor() : base( + MemberValidator.ArgumentsMatch("AreEqual", + ArgumentValidator.IsType(TypeSelector.GetFloatType), + ArgumentValidator.IsType(TypeSelector.GetFloatType), + ArgumentValidator.IsType(TypeSelector.GetFloatType)) + ) + { + } + } + + // public static void AreEqual(double expected, double actual, double delta) + public class AssertDoubleAreEqualWithDeltaSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertDoubleAreEqualWithDeltaSyntaxVisitor() : base( + MemberValidator.ArgumentsMatch("AreEqual", + ArgumentValidator.IsType(TypeSelector.GetDoubleType), + ArgumentValidator.IsType(TypeSelector.GetDoubleType), + ArgumentValidator.IsType(TypeSelector.GetDoubleType)) + ) + { + } + } + + // public static void AreEqual(string expected, string actual, bool ignoreCase, CultureInfo culture + public class AssertStringAreEqualSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertStringAreEqualSyntaxVisitor() : base( + MemberValidator.ArgumentsMatch("AreEqual", + ArgumentValidator.IsType(TypeSelector.GetStringType), + ArgumentValidator.IsType(TypeSelector.GetStringType), + ArgumentValidator.IsType(TypeSelector.GetBooleanType))) + { + } + } + + // public static void AreEqual(T expected, T actual) + // public static void AreEqual(object expected, object actual) + public class AssertObjectAreEqualSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertObjectAreEqualSyntaxVisitor() : base(new MemberValidator("AreEqual")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertAreEqualCodeFix)), Shared] + public class AssertAreEqualCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertAreEqualAnalyzer.DiagnosticId); + + protected override async Task GetNewExpressionAsync(ExpressionSyntax expression, Document document, FluentAssertionsDiagnosticProperties properties, CancellationToken cancellationToken) + { + switch (properties.VisitorName) + { + case nameof(AssertAreEqualAnalyzer.AssertFloatAreEqualWithDeltaSyntaxVisitor): + case nameof(AssertAreEqualAnalyzer.AssertDoubleAreEqualWithDeltaSyntaxVisitor): + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreEqual", "BeApproximately", "Assert"); + case nameof(AssertAreEqualAnalyzer.AssertObjectAreEqualSyntaxVisitor): + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreEqual", "Be", "Assert"); + case nameof(AssertAreEqualAnalyzer.AssertStringAreEqualSyntaxVisitor): + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + return GetNewExpressionForAreNotEqualOrAreEqualStrings(expression, semanticModel, "AreEqual", "Be", "BeEquivalentTo", "Assert"); + default: + throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); + } + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreNotEqual.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreNotEqual.cs new file mode 100644 index 00000000..a3030fe2 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreNotEqual.cs @@ -0,0 +1,105 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using TypeSelector = FluentAssertions.Analyzers.Utilities.SemanticModelTypeExtensions; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertAreNotEqualAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertAreNotEqual; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotBeApproximately() for complex numbers and .Should().NotBe() for other cases."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertFloatAreNotEqualWithDeltaSyntaxVisitor(); + yield return new AssertDoubleAreNotEqualWithDeltaSyntaxVisitor(); + yield return new AssertStringAreNotEqualSyntaxVisitor(); + yield return new AssertObjectAreNotEqualSyntaxVisitor(); + } + } + + // public static void AreNotEqual(float expected, float actual, float delta) + public class AssertFloatAreNotEqualWithDeltaSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertFloatAreNotEqualWithDeltaSyntaxVisitor() : base( + MemberValidator.ArgumentsMatch("AreNotEqual", + ArgumentValidator.IsType(TypeSelector.GetFloatType), + ArgumentValidator.IsType(TypeSelector.GetFloatType), + ArgumentValidator.IsType(TypeSelector.GetFloatType)) + ) + { + } + } + + // public static void AreNotEqual(double expected, double actual, double delta) + public class AssertDoubleAreNotEqualWithDeltaSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertDoubleAreNotEqualWithDeltaSyntaxVisitor() : base( + MemberValidator.ArgumentsMatch("AreNotEqual", + ArgumentValidator.IsType(TypeSelector.GetDoubleType), + ArgumentValidator.IsType(TypeSelector.GetDoubleType), + ArgumentValidator.IsType(TypeSelector.GetDoubleType)) + ) + { + } + } + + // public static void AreNotEqual(string expected, string actual, bool ignoreCase, CultureInfo culture + public class AssertStringAreNotEqualSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertStringAreNotEqualSyntaxVisitor() : base( + MemberValidator.ArgumentsMatch("AreNotEqual", + ArgumentValidator.IsType(TypeSelector.GetStringType), + ArgumentValidator.IsType(TypeSelector.GetStringType), + ArgumentValidator.IsType(TypeSelector.GetBooleanType)) + ) + { + } + } + + // public static void AreNotEqual(T expected, T actual) + // public static void AreNotEqual(object expected, object actual) + public class AssertObjectAreNotEqualSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertObjectAreNotEqualSyntaxVisitor() : base(new MemberValidator("AreNotEqual")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertAreNotEqualCodeFix)), Shared] + public class AssertAreNotEqualCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertAreNotEqualAnalyzer.DiagnosticId); + + protected override async Task GetNewExpressionAsync(ExpressionSyntax expression, Document document, FluentAssertionsDiagnosticProperties properties, CancellationToken cancellationToken) + { + switch (properties.VisitorName) + { + case nameof(AssertAreNotEqualAnalyzer.AssertFloatAreNotEqualWithDeltaSyntaxVisitor): + case nameof(AssertAreNotEqualAnalyzer.AssertDoubleAreNotEqualWithDeltaSyntaxVisitor): + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreNotEqual", "NotBeApproximately", "Assert"); + case nameof(AssertAreNotEqualAnalyzer.AssertObjectAreNotEqualSyntaxVisitor): + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreNotEqual", "NotBe", "Assert"); + case nameof(AssertAreNotEqualAnalyzer.AssertStringAreNotEqualSyntaxVisitor): + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + return GetNewExpressionForAreNotEqualOrAreEqualStrings(expression, semanticModel, "AreNotEqual", "NotBe", "NotBeEquivalentTo", "Assert"); + default: + throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); + } + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreNotSame.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreNotSame.cs new file mode 100644 index 00000000..57199778 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreNotSame.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertAreNotSameAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertAreNotSame; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotBeSameAs() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertAreNotSameSyntaxVisitor(); + } + } + + public class AssertAreNotSameSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertAreNotSameSyntaxVisitor() : base(new MemberValidator("AreNotSame")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertAreNotSameCodeFix)), Shared] + public class AssertAreNotSameCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertAreNotSameAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + => RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreNotSame", "NotBeSameAs", "Assert"); + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreSame.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreSame.cs new file mode 100644 index 00000000..fadc4518 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertAreSame.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertAreSameAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertAreSame; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeSameAs() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertAreSameSyntaxVisitor(); + } + } + + public class AssertAreSameSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertAreSameSyntaxVisitor() : base(new MemberValidator("AreSame")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertAreSameCodeFix)), Shared] + public class AssertAreSameCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertAreSameAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + => RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreSame", "BeSameAs", "Assert"); + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsFalse.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsFalse.cs new file mode 100644 index 00000000..ed40c402 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsFalse.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertIsFalseAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertIsFalse; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeFalse() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertIsFalseSyntaxVisitor(); + } + } + + public class AssertIsFalseSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertIsFalseSyntaxVisitor() : base(new MemberValidator("IsFalse")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertIsFalseCodeFix)), Shared] + public class AssertIsFalseCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionShouldBeEmptyAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + => RenameMethodAndReplaceWithSubjectShould(expression, "IsFalse", "BeFalse", "Assert"); + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsInstanceOfType.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsInstanceOfType.cs new file mode 100644 index 00000000..a846ca9f --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsInstanceOfType.cs @@ -0,0 +1,67 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertIsInstanceOfTypeAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertIsInstanceOfType; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeOfType() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertIsInstanceOfTypeSyntaxVisitor(); + } + } + + public class AssertIsInstanceOfTypeSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertIsInstanceOfTypeSyntaxVisitor() : base(new MemberValidator("IsInstanceOfType")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertIsInstanceOfTypeCodeFix)), Shared] + public class AssertIsInstanceOfTypeCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertIsInstanceOfTypeAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + var newExpression = RenameMethodAndReplaceWithSubjectShould(expression, "IsInstanceOfType", "BeOfType", "Assert"); + + var beOfType = newExpression.DescendantNodes() + .OfType() + .First(node => node.Name.Identifier.Text == "BeOfType"); + + if (beOfType.Parent is InvocationExpressionSyntax invocation) + { + var arguments = invocation.ArgumentList.Arguments; + if (arguments.Any() && arguments[0].Expression is TypeOfExpressionSyntax typeOfExpression) + { + var genericBeOfType = beOfType.WithName(SF.GenericName(beOfType.Name.Identifier.Text) + .AddTypeArgumentListArguments(typeOfExpression.Type) + ); + newExpression = newExpression.ReplaceNode(beOfType, genericBeOfType); + return GetNewExpression(newExpression, NodeReplacement.RemoveFirstArgument("BeOfType")); + } + } + + return newExpression; + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotInstanceOfType.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotInstanceOfType.cs new file mode 100644 index 00000000..9a246b57 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotInstanceOfType.cs @@ -0,0 +1,67 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertIsNotInstanceOfTypeAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertIsNotInstanceOfType; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotBeOfType() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertIsNotInstanceOfTypeSyntaxVisitor(); + } + } + + public class AssertIsNotInstanceOfTypeSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertIsNotInstanceOfTypeSyntaxVisitor() : base(new MemberValidator("IsNotInstanceOfType")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertIsNotInstanceOfTypeCodeFix)), Shared] + public class AssertIsNotInstanceOfTypeCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertIsNotInstanceOfTypeAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + var newExpression = RenameMethodAndReplaceWithSubjectShould(expression, "IsNotInstanceOfType", "NotBeOfType", "Assert"); + + var beOfType = newExpression.DescendantNodes() + .OfType() + .First(node => node.Name.Identifier.Text == "NotBeOfType"); + + if (beOfType.Parent is InvocationExpressionSyntax invocation) + { + var arguments = invocation.ArgumentList.Arguments; + if (arguments.Any() && arguments[0].Expression is TypeOfExpressionSyntax typeOfExpression) + { + var genericBeOfType = beOfType.WithName(SF.GenericName(beOfType.Name.Identifier.Text) + .AddTypeArgumentListArguments(typeOfExpression.Type) + ); + newExpression = newExpression.ReplaceNode(beOfType, genericBeOfType); + return GetNewExpression(newExpression, NodeReplacement.RemoveFirstArgument("NotBeOfType")); + } + } + + return newExpression; + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotNull.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotNull.cs new file mode 100644 index 00000000..dd5a2273 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotNull.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertIsNotNullAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertIsNotNull; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotBeNull() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertIsNotNullSyntaxVisitor(); + } + } + + public class AssertIsNotNullSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertIsNotNullSyntaxVisitor() : base(new MemberValidator("IsNotNull")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertIsNotNullCodeFix)), Shared] + public class AssertIsNotNullCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertIsNotNullAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + => RenameMethodAndReplaceWithSubjectShould(expression, "IsNotNull", "NotBeNull", "Assert"); + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNull.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNull.cs new file mode 100644 index 00000000..e6222c42 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNull.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertIsNullAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertIsNull; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeNull() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertIsNullSyntaxVisitor(); + } + } + + public class AssertIsNullSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertIsNullSyntaxVisitor() : base(new MemberValidator("IsNull")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertIsNullCodeFix)), Shared] + public class AssertIsNullCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertIsNullAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + => RenameMethodAndReplaceWithSubjectShould(expression, "IsNull", "BeNull", "Assert"); + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsTrue.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsTrue.cs new file mode 100644 index 00000000..1cd0c784 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsTrue.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertIsTrueAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertIsTrue; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeTrue() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertIsTrueSyntaxVisitor(); + } + } + + public class AssertIsTrueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertIsTrueSyntaxVisitor() : base(new MemberValidator("IsTrue")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertIsTrueCodeFix)), Shared] + public class AssertIsTrueCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionShouldBeEmptyAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + => RenameMethodAndReplaceWithSubjectShould(expression, "IsTrue", "BeTrue", "Assert"); + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertThrowsException.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertThrowsException.cs new file mode 100644 index 00000000..f3a042c1 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertThrowsException.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertThrowsExceptionAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertThrowsException; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().ThrowExactly() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertThrowsExceptionSyntaxVisitor(); + } + } + + public class AssertThrowsExceptionSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertThrowsExceptionSyntaxVisitor() : base(new MemberValidator("ThrowsException")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertThrowsExceptionCodeFix)), Shared] + public class AssertThrowsExceptionCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertThrowsExceptionAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + => RenameMethodAndReplaceWithSubjectShould(expression, "ThrowsException", "ThrowExactly", "Assert"); + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertThrowsExceptionAsync.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertThrowsExceptionAsync.cs new file mode 100644 index 00000000..a77b2978 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertThrowsExceptionAsync.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AssertThrowsExceptionAsyncAnalyzer : MsTestAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.AssertThrowsExceptionAsync; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().ThrowExactlyAsync() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new AssertThrowsExceptionAsyncSyntaxVisitor(); + } + } + + public class AssertThrowsExceptionAsyncSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertThrowsExceptionAsyncSyntaxVisitor() : base(new MemberValidator("ThrowsExceptionAsync")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertThrowsExceptionAsyncCodeFix)), Shared] + public class AssertThrowsExceptionAsyncCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertThrowsExceptionAsyncAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + => RenameMethodAndReplaceWithSubjectShould(expression, "ThrowsExceptionAsync", "ThrowExactlyAsync", "Assert"); + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreInstancesOfType.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreInstancesOfType.cs new file mode 100644 index 00000000..1f84710b --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreInstancesOfType.cs @@ -0,0 +1,57 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertAllItemsAreInstancesOfTypeAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertAllItemsAreInstancesOfType; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().AllBeOfType() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertAllItemsAreInstancesOfTypeSyntaxVisitor(); + } + } + + public class CollectionAssertAllItemsAreInstancesOfTypeSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertAllItemsAreInstancesOfTypeSyntaxVisitor() : base(new MemberValidator("AllItemsAreInstancesOfType")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertAllItemsAreInstancesOfTypeCodeFix)), Shared] + public class CollectionAssertAllItemsAreInstancesOfTypeCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertAllItemsAreInstancesOfTypeAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + var newExpression = RenameMethodAndReplaceWithSubjectShould(expression, "AllItemsAreInstancesOfType", "AllBeOfType", "CollectionAssert"); + + var argumentsReplacer = NodeReplacement.RemoveFirstArgument("AllBeOfType"); + var possibleNewExpression = GetNewExpression(newExpression, argumentsReplacer); + + if (argumentsReplacer.Argument.Expression is TypeOfExpressionSyntax typeOfExpression) + { + var addTypeArgument = NodeReplacement.AddTypeArgument("AllBeOfType", typeOfExpression.Type); + return GetNewExpression(possibleNewExpression, addTypeArgument); + } + + return newExpression; + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreNotNull.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreNotNull.cs new file mode 100644 index 00000000..589d4a58 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreNotNull.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertAllItemsAreNotNullAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertAllItemsAreNotNull; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotContainNulls() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertAllItemsAreNotNullSyntaxVisitor(); + } + } + + public class CollectionAssertAllItemsAreNotNullSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertAllItemsAreNotNullSyntaxVisitor() : base(new MemberValidator("AllItemsAreNotNull")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertAllItemsAreNotNullCodeFix)), Shared] + public class CollectionAssertAllItemsAreNotNullCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertAllItemsAreNotNullAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReplaceWithSubjectShould(expression, "AllItemsAreNotNull", "NotContainNulls", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreUnique.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreUnique.cs new file mode 100644 index 00000000..14aec7bd --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAllItemsAreUnique.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertAllItemsAreUniqueAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertAllItemsAreUnique; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().OnlyHaveUniqueItems() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertAllItemsAreUniqueSyntaxVisitor(); + } + } + + public class CollectionAssertAllItemsAreUniqueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertAllItemsAreUniqueSyntaxVisitor() : base(new MemberValidator("AllItemsAreUnique")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertAllItemsAreUniqueCodeFix)), Shared] + public class CollectionAssertAllItemsAreUniqueCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertAllItemsAreUniqueAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReplaceWithSubjectShould(expression, "AllItemsAreUnique", "OnlyHaveUniqueItems", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreEqual.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreEqual.cs new file mode 100644 index 00000000..6c6ed557 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreEqual.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertAreEqualAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertAreEqual; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().Equal() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertAreEqualSyntaxVisitor(); + } + } + + public class CollectionAssertAreEqualSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertAreEqualSyntaxVisitor() : base(new MemberValidator("AreEqual")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertAreEqualCodeFix)), Shared] + public class CollectionAssertAreEqualCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertAreEqualAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreEqual", "Equal", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreEquivalent.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreEquivalent.cs new file mode 100644 index 00000000..43f5eb4b --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreEquivalent.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertAreEquivalentAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertAreEquivalent; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeEquivalentTo() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertAreEquivalentSyntaxVisitor(); + } + } + + public class CollectionAssertAreEquivalentSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertAreEquivalentSyntaxVisitor() : base(new MemberValidator("AreEquivalent")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertAreEquivalentCodeFix)), Shared] + public class CollectionAssertAreEquivalentCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertAreEquivalentAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreEquivalent", "BeEquivalentTo", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreNotEqual.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreNotEqual.cs new file mode 100644 index 00000000..b01a6bad --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreNotEqual.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertAreNotEqualAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertAreNotEqual; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().AreNotEqual() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertAreNotEqualSyntaxVisitor(); + } + } + + public class CollectionAssertAreNotEqualSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertAreNotEqualSyntaxVisitor() : base(new MemberValidator("AreNotEqual")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertAreNotEqualCodeFix)), Shared] + public class CollectionAssertAreNotEqualCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertAreNotEqualAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreNotEqual", "NotEqual", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreNotEquivalent.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreNotEquivalent.cs new file mode 100644 index 00000000..0e9167ba --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertAreNotEquivalent.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertAreNotEquivalentAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertAreNotEquivalent; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotBeEquivalentTo() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertAreNotEquivalentSyntaxVisitor(); + } + } + + public class CollectionAssertAreNotEquivalentSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertAreNotEquivalentSyntaxVisitor() : base(new MemberValidator("AreNotEquivalent")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertAreNotEquivalentCodeFix)), Shared] + public class CollectionAssertAreNotEquivalentCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertAreNotEquivalentAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "AreNotEquivalent", "NotBeEquivalentTo", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertContains.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertContains.cs new file mode 100644 index 00000000..952f13da --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertContains.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertContainsAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertContains; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().Contain() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertContainsSyntaxVisitor(); + } + } + + public class CollectionAssertContainsSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertContainsSyntaxVisitor() : base(new MemberValidator("Contains")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertContainsCodeFix)), Shared] + public class CollectionAssertContainsCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertContainsAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReplaceWithSubjectShould(expression, "Contains", "Contain", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertDoesNotContain.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertDoesNotContain.cs new file mode 100644 index 00000000..33ac849c --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertDoesNotContain.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertDoesNotContainAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertDoesNotContain; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotContain() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertDoesNotContainSyntaxVisitor(); + } + } + + public class CollectionAssertDoesNotContainSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertDoesNotContainSyntaxVisitor() : base(new MemberValidator("DoesNotContain")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertDoesNotContainCodeFix)), Shared] + public class CollectionAssertDoesNotContainCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertDoesNotContainAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReplaceWithSubjectShould(expression, "DoesNotContain", "NotContain", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertIsNotSubsetOf.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertIsNotSubsetOf.cs new file mode 100644 index 00000000..546e27fc --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertIsNotSubsetOf.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertIsNotSubsetOfAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertIsNotSubsetOf; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotBeSubsetOf() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertIsNotSubsetOfSyntaxVisitor(); + } + } + + public class CollectionAssertIsNotSubsetOfSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertIsNotSubsetOfSyntaxVisitor() : base(new MemberValidator("IsNotSubsetOf")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertIsNotSubsetOfCodeFix)), Shared] + public class CollectionAssertIsNotSubsetOfCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertIsNotSubsetOfAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsNotSubsetOf", "NotBeSubsetOf", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertIsSubsetOf.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertIsSubsetOf.cs new file mode 100644 index 00000000..01e9c0b9 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/CollectionAssertIsSubsetOf.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class CollectionAssertIsSubsetOfAnalyzer : MsTestCollectionAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.CollectionAssertIsSubsetOf; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeSubsetOf() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new CollectionAssertIsSubsetOfSyntaxVisitor(); + } + } + + public class CollectionAssertIsSubsetOfSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public CollectionAssertIsSubsetOfSyntaxVisitor() : base(new MemberValidator("IsSubsetOf")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionAssertIsSubsetOfCodeFix)), Shared] + public class CollectionAssertIsSubsetOfCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(CollectionAssertIsSubsetOfAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsSubsetOf", "BeSubsetOf", "CollectionAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/MsTestBase.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/MsTestBase.cs new file mode 100644 index 00000000..478545d8 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/MsTestBase.cs @@ -0,0 +1,93 @@ +using FluentAssertions.Analyzers.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Linq; + +namespace FluentAssertions.Analyzers +{ + public abstract class MsTestAnalyzer : FluentAssertionsAnalyzer + { + private static readonly NameSyntax MsTestNamespace = SyntaxFactory.ParseName("Microsoft.VisualStudio.TestTools.UnitTesting"); + + protected override bool ShouldAnalyzeMethod(MethodDeclarationSyntax method) + { + var compilation = method.FirstAncestorOrSelf(); + + if (compilation == null) return false; + + return compilation.Usings.Any(usingDirective => usingDirective.Name.IsEquivalentTo(MsTestNamespace)); + } + } + + public abstract class MsTestAssertAnalyzer : MsTestAnalyzer + { + protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel) => type.Name == "Assert"; + } + + public abstract class MsTestStringAssertAnalyzer : MsTestAnalyzer + { + protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel) => type.Name == "StringAssert"; + } + + public abstract class MsTestCollectionAssertAnalyzer : MsTestAnalyzer + { + protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel) => type.Name == "CollectionAssert"; + } + + public abstract class MsTestCodeFixProvider : FluentAssertionsCodeFixProvider + { + protected ExpressionSyntax RenameMethodAndReplaceWithSubjectShould(ExpressionSyntax expression, string oldName, string newName, string assert) + { + var rename = NodeReplacement.RenameAndRemoveFirstArgument(oldName, newName); + var newExpression = GetNewExpression(expression, rename); + + return ReplaceIdentifier(newExpression, assert, Expressions.SubjectShould(rename.Argument.Expression)); + } + + protected ExpressionSyntax RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(ExpressionSyntax expression, string oldName, string newName, string assert) + { + var rename = NodeReplacement.RenameAndExtractArguments(oldName, newName); + var newExpression = GetNewExpression(expression, rename); + + var actual = rename.Arguments[1]; + + newExpression = ReplaceIdentifier(newExpression, assert, Expressions.SubjectShould(actual.Expression)); + + return GetNewExpression(newExpression, NodeReplacement.WithArguments(newName, rename.Arguments.RemoveAt(1))); + } + + protected ExpressionSyntax GetNewExpressionForAreNotEqualOrAreEqualStrings(ExpressionSyntax expression, SemanticModel semanticModel, string oldName, string newName, string newNameIgnoreCase, string assert) + { + var rename = NodeReplacement.RenameAndExtractArguments(oldName, newName); + var newExpression = GetNewExpression(expression, rename); + + var actual = rename.Arguments[1]; + + newExpression = ReplaceIdentifier(newExpression, assert, Expressions.SubjectShould(actual.Expression)); + + var newArguments = rename.Arguments.Remove(actual); + + var ignoreCase = false; + if (newArguments.Count >= 2 && newArguments[1].Expression is LiteralExpressionSyntax possibleIgnoreCaseArg) + { + newArguments = newArguments.Remove(newArguments[1]); + ignoreCase = possibleIgnoreCaseArg.Token.IsKind(SyntaxKind.TrueKeyword); + } + + if (newArguments.Count >= 2 && semanticModel.GetCultureInfoType().Equals(semanticModel.GetTypeInfo(rename.Arguments[3].Expression).Type)) + { + newArguments = newArguments.Remove(newArguments[1]); + } + + newExpression = GetNewExpression(newExpression, NodeReplacement.WithArguments(newName, newArguments)); + + if (ignoreCase) + { + return GetNewExpression(newExpression, NodeReplacement.Rename(newName, newNameIgnoreCase)); + } + + return newExpression; + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertContains.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertContains.cs new file mode 100644 index 00000000..d99d65ed --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertContains.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class StringAssertContainsAnalyzer : MsTestStringAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.StringAssertContains; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().Contain() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new StringAssertContainsSyntaxVisitor(); + } + } + + public class StringAssertContainsSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public StringAssertContainsSyntaxVisitor() : base(new MemberValidator("Contains")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(StringAssertContainsCodeFix)), Shared] + public class StringAssertContainsCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(StringAssertContainsAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "Contains", "Contain", "StringAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertDoesNotMatch.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertDoesNotMatch.cs new file mode 100644 index 00000000..201e0abe --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertDoesNotMatch.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class StringAssertDoesNotMatchAnalyzer : MsTestStringAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.StringAssertDoesNotMatch; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotMatchRegex() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new StringAssertDoesNotMatchSyntaxVisitor(); + } + } + + public class StringAssertDoesNotMatchSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public StringAssertDoesNotMatchSyntaxVisitor() : base(new MemberValidator("DoesNotMatch")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(StringAssertDoesNotMatchCodeFix)), Shared] + public class StringAssertDoesNotMatchCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(StringAssertDoesNotMatchAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReplaceWithSubjectShould(expression, "DoesNotMatch", "NotMatchRegex", "StringAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertEndsWith.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertEndsWith.cs new file mode 100644 index 00000000..6d4d3322 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertEndsWith.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class StringAssertEndsWithAnalyzer : MsTestStringAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.StringAssertEndsWith; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().EndWith() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new StringAssertEndsWithSyntaxVisitor(); + } + } + + public class StringAssertEndsWithSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public StringAssertEndsWithSyntaxVisitor() : base(new MemberValidator("EndsWith")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(StringAssertEndsWithCodeFix)), Shared] + public class StringAssertEndsWithCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(StringAssertEndsWithAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "EndsWith", "EndWith", "StringAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertMatches.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertMatches.cs new file mode 100644 index 00000000..5a1de0fe --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertMatches.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class StringAssertMatchesAnalyzer : MsTestStringAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.StringAssertMatches; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().MatchRegex() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new StringAssertMatchesSyntaxVisitor(); + } + } + + public class StringAssertMatchesSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public StringAssertMatchesSyntaxVisitor() : base(new MemberValidator("Matches")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(StringAssertMatchesCodeFix)), Shared] + public class StringAssertMatchesCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(StringAssertMatchesAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReplaceWithSubjectShould(expression, "Matches", "MatchRegex", "StringAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertStartsWith.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertStartsWith.cs new file mode 100644 index 00000000..60b393b6 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/StringAssertStartsWith.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; + +namespace FluentAssertions.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class StringAssertStartsWithAnalyzer : MsTestStringAssertAnalyzer + { + public const string DiagnosticId = Constants.Tips.MsTest.StringAssertStartsWith; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().StartWith() instead."; + + protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + protected override IEnumerable Visitors + { + get + { + yield return new StringAssertStartsWithSyntaxVisitor(); + } + } + + public class StringAssertStartsWithSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public StringAssertStartsWithSyntaxVisitor() : base(new MemberValidator("StartsWith")) + { + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(StringAssertStartsWithCodeFix)), Shared] + public class StringAssertStartsWithCodeFix : MsTestCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(StringAssertStartsWithAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) + { + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "StartsWith", "StartWith", "StringAssert"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/Numerics/NumericShouldBeApproximately.cs b/src/FluentAssertions.Analyzers/Tips/Numerics/NumericShouldBeApproximately.cs index 785eb57c..84ed4f29 100644 --- a/src/FluentAssertions.Analyzers/Tips/Numerics/NumericShouldBeApproximately.cs +++ b/src/FluentAssertions.Analyzers/Tips/Numerics/NumericShouldBeApproximately.cs @@ -38,7 +38,7 @@ public class MathAbsShouldBeLessOrEqualToSyntaxVisitor : FluentAssertionsCSharpS { } - private static bool IsSubtractExpression(SeparatedSyntaxList arguments) + private static bool IsSubtractExpression(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (arguments.Count != 1) return false; diff --git a/src/FluentAssertions.Analyzers/Tips/ShouldEquals.cs b/src/FluentAssertions.Analyzers/Tips/ShouldEquals.cs index f335f337..8811b18b 100644 --- a/src/FluentAssertions.Analyzers/Tips/ShouldEquals.cs +++ b/src/FluentAssertions.Analyzers/Tips/ShouldEquals.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using System.Composition; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; diff --git a/src/FluentAssertions.Analyzers/Utilities/Expressions.cs b/src/FluentAssertions.Analyzers/Utilities/Expressions.cs new file mode 100644 index 00000000..a9d95032 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Utilities/Expressions.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace FluentAssertions.Analyzers +{ + public static class Expressions + { + public static InvocationExpressionSyntax SubjectShould(ExpressionSyntax subject) + { + return SF.InvocationExpression( + SF.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, subject, SF.IdentifierName("Should")), + SF.ArgumentList() + ); + } + } +} diff --git a/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsAnalyzer.cs b/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsAnalyzer.cs index 77948bcb..3639bf32 100644 --- a/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsAnalyzer.cs +++ b/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsAnalyzer.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; namespace FluentAssertions.Analyzers @@ -30,6 +29,8 @@ private void AnalyzeCodeBlock(CodeBlockAnalysisContext context) var method = context.CodeBlock as MethodDeclarationSyntax; if (method == null) return; + if (!ShouldAnalyzeMethod(method)) return; + if (method.Body != null) { foreach (var statement in method.Body.Statements.OfType()) @@ -52,6 +53,8 @@ private void AnalyzeCodeBlock(CodeBlockAnalysisContext context) } } + protected virtual bool ShouldAnalyzeMethod(MethodDeclarationSyntax method) => true; + protected virtual bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel) => true; protected virtual Diagnostic AnalyzeExpression(ExpressionSyntax expression, SemanticModel semanticModel) @@ -66,6 +69,7 @@ protected virtual Diagnostic AnalyzeExpression(ExpressionSyntax expression, Sema foreach (var visitor in Visitors) { + visitor.SemanticModel = semanticModel; expression.Accept(visitor); if (visitor.IsValid(expression)) diff --git a/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCSharpSyntaxVisitor.cs b/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCSharpSyntaxVisitor.cs index 1d31c0e5..b3954663 100644 --- a/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCSharpSyntaxVisitor.cs +++ b/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCSharpSyntaxVisitor.cs @@ -13,6 +13,7 @@ public class FluentAssertionsCSharpSyntaxVisitor : CSharpSyntaxVisitor public ImmutableStack AllMembers { get; } public ImmutableStack Members { get; private set; } + public SemanticModel SemanticModel { get; set; } public virtual bool IsValid(ExpressionSyntax expression) => Members.IsEmpty; @@ -45,7 +46,7 @@ public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax no else if (node.Parent is InvocationExpressionSyntax invocation) { var member = Members.Peek(); - if (member.Name == name && member.AreArgumentsValid(invocation.ArgumentList.Arguments)) + if (member.Name == name && member.MatchesInvocationExpression(invocation, SemanticModel)) { Members = Members.Pop(); } @@ -79,7 +80,7 @@ public override void VisitElementAccessExpression(ElementAccessExpressionSyntax } var member = Members.Peek(); - if (member.Name == name && member.AreArgumentsValid(node.ArgumentList.Arguments)) + if (member.Name == name && member.MatchesElementAccessExpression(node, SemanticModel)) { Members = Members.Pop(); } diff --git a/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCodeFixProvider.cs b/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCodeFixProvider.cs index 0c7b656a..f768bc85 100644 --- a/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCodeFixProvider.cs +++ b/src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCodeFixProvider.cs @@ -57,12 +57,11 @@ protected ExpressionSyntax GetNewExpression(ExpressionSyntax expression, params foreach (var replacement in replacements) { newStatement = GetNewExpression(newStatement, replacement); - var code = newStatement.ToFullString(); } return newStatement; } - protected ExpressionSyntax GetNewExpression(ExpressionSyntax expression, NodeReplacement replacement) + protected static ExpressionSyntax GetNewExpression(ExpressionSyntax expression, NodeReplacement replacement) { var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor(); expression.Accept(visitor); @@ -86,7 +85,7 @@ protected ExpressionSyntax GetNewExpression(ExpressionSyntax expression, NodeRep throw new System.InvalidOperationException("should not get here"); } - protected ExpressionSyntax RenameIdentifier(ExpressionSyntax expression, string oldName, string newName) + protected static ExpressionSyntax RenameIdentifier(ExpressionSyntax expression, string oldName, string newName) { var identifierNode = expression.DescendantNodes() .OfType() @@ -106,5 +105,13 @@ private Task GetNewExpressionSafelyAsync(ExpressionSyntax expr return Task.FromResult(expression); } } + + protected static ExpressionSyntax ReplaceIdentifier(ExpressionSyntax expression, string name, ExpressionSyntax identifierReplacement) + { + var identifierNode = expression.DescendantNodes() + .OfType() + .First(node => node.Identifier.Text == name); + return expression.ReplaceNode(identifierNode, identifierReplacement.WithTriviaFrom(identifierNode)); + } } } diff --git a/src/FluentAssertions.Analyzers/Utilities/MemberValidator.cs b/src/FluentAssertions.Analyzers/Utilities/MemberValidator.cs index a776f0b4..e521b58e 100644 --- a/src/FluentAssertions.Analyzers/Utilities/MemberValidator.cs +++ b/src/FluentAssertions.Analyzers/Utilities/MemberValidator.cs @@ -5,22 +5,36 @@ namespace FluentAssertions.Analyzers { + public delegate bool ArgumentsPredicate(SeparatedSyntaxList arguments, SemanticModel semanticModel); + public delegate bool ArgumentPredicate(ArgumentSyntax argument, SemanticModel semanticModel); + [DebuggerDisplay("{Name}")] public class MemberValidator { + private readonly ArgumentsPredicate _argumentsPredicate; + public string Name { get; } - public Func, bool> AreArgumentsValid { get; } public MemberValidator(string name) { Name = name; - AreArgumentsValid = _ => true; + _argumentsPredicate = (arguments, semanticModel) => true; } - public MemberValidator(string name, Func, bool> argumentsPredicate) + public MemberValidator(string name, ArgumentsPredicate argumentsPredicate) { Name = name; - AreArgumentsValid = argumentsPredicate; + _argumentsPredicate = argumentsPredicate; + } + + public bool MatchesInvocationExpression(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + { + return _argumentsPredicate(invocation.ArgumentList.Arguments, semanticModel); + } + + public bool MatchesElementAccessExpression(ElementAccessExpressionSyntax elementAccess, SemanticModel semanticModel) + { + return _argumentsPredicate(elementAccess.ArgumentList.Arguments, semanticModel); } public static MemberValidator And { get; } = new MemberValidator(nameof(And)); @@ -30,24 +44,25 @@ public MemberValidator(string name, Func, bo public static MemberValidator MathodNotContainingLambda(string name) => new MemberValidator(name, MethodNotContainingLambdaPredicate); public static MemberValidator MathodContainingLambda(string name) => new MemberValidator(name, MethodContainingLambdaPredicate); public static MemberValidator ArgumentIsVariable(string name) => new MemberValidator(name, ArgumentIsVariablePredicate); - public static MemberValidator ArgumentIsLiteral(string name, T value) => new MemberValidator(name, arguments => ArgumentIsLiteralPredicate(arguments, value)); + public static MemberValidator ArgumentIsLiteral(string name, T value) => new MemberValidator(name, (arguments, semanticModel) => ArgumentIsLiteralPredicate(arguments, value)); public static MemberValidator ArgumentIsIdentifierOrLiteral(string name) => new MemberValidator(name, ArgumentIsIdentifierOrLiteralPredicate); - public static MemberValidator HasArguments(string name) => new MemberValidator(name, arguments => arguments.Any()); - public static MemberValidator HasNoArguments(string name) => new MemberValidator(name, arguments => !arguments.Any()); + public static MemberValidator HasArguments(string name) => new MemberValidator(name, (arguments, semanticModel) => arguments.Any()); + public static MemberValidator HasNoArguments(string name) => new MemberValidator(name, (arguments, semanticModel) => !arguments.Any()); + public static MemberValidator ArgumentsMatch(string name, params ArgumentPredicate[] predicates) => new MemberValidator(name, (arguments, semanticModel) => ArgumentsMatchPredicate(arguments, predicates, semanticModel)); - public static bool MethodNotContainingLambdaPredicate(SeparatedSyntaxList arguments) + public static bool MethodNotContainingLambdaPredicate(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return true; return !(arguments.First().Expression is LambdaExpressionSyntax); } - public static bool MethodContainingLambdaPredicate(SeparatedSyntaxList arguments) + public static bool MethodContainingLambdaPredicate(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; return arguments.First().Expression is LambdaExpressionSyntax; } - public static bool ArgumentIsVariablePredicate(SeparatedSyntaxList arguments) + public static bool ArgumentIsVariablePredicate(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; @@ -72,13 +87,32 @@ public static bool ArgumentIsLiteralPredicate(SeparatedSyntaxList arguments) + public static bool ArgumentIsIdentifierOrLiteralPredicate(SeparatedSyntaxList arguments, SemanticModel semanticModel) { if (!arguments.Any()) return false; var argumentsExpression = arguments.First().Expression; return argumentsExpression is IdentifierNameSyntax || argumentsExpression is LiteralExpressionSyntax; } + public static bool ArgumentsMatchPredicate(SeparatedSyntaxList arguments, ArgumentPredicate[] validators, SemanticModel semanticModel) + { + for (int i = 0; i < validators.Length && i < arguments.Count; i++) + { + if (!validators[i](arguments[i], semanticModel)) return false; + } + + return true; + } + } + public class ArgumentValidator + { + public static ArgumentPredicate IsType(Func typeSelector) + { + return (argument, semanticModel) => + { + return semanticModel.GetTypeInfo(argument.Expression).Type.Equals(typeSelector(semanticModel)); + }; + } } } \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Utilities/NodeReplacements/AddTypeArgumentNodeReplacement.cs b/src/FluentAssertions.Analyzers/Utilities/NodeReplacements/AddTypeArgumentNodeReplacement.cs new file mode 100644 index 00000000..f14736b4 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Utilities/NodeReplacements/AddTypeArgumentNodeReplacement.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Diagnostics; + +using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace FluentAssertions.Analyzers +{ + [DebuggerDisplay("AddTypeArgument(name: \"{_name}\", type: \"_type\")")] + public class AddTypeArgumentNodeReplacement : EditNodeReplacement + { + private readonly TypeSyntax _type; + + public AddTypeArgumentNodeReplacement(string name, TypeSyntax type) : base(name) => _type = type; + + public override InvocationExpressionSyntax ComputeNew(InvocationExpressionSyntax node) + { + var memberAccessExpression = node.Expression as MemberAccessExpressionSyntax; + var newExpression = memberAccessExpression.WithName(SF.GenericName( + memberAccessExpression.Name.Identifier, SF.TypeArgumentList( + new SeparatedSyntaxList().Add(_type)) + )); + return node.WithExpression(newExpression); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Utilities/NodeReplacements/NodeReplacement.cs b/src/FluentAssertions.Analyzers/Utilities/NodeReplacements/NodeReplacement.cs index c9d7ffed..71449dad 100644 --- a/src/FluentAssertions.Analyzers/Utilities/NodeReplacements/NodeReplacement.cs +++ b/src/FluentAssertions.Analyzers/Utilities/NodeReplacements/NodeReplacement.cs @@ -26,5 +26,6 @@ public virtual void ExtractValues(MemberAccessExpressionSyntax node) { } public static RemoveAndRetrieveIndexerArgumentsNodeReplacement RemoveAndRetrieveIndexerArguments(string methodAfterIndexer) => new RemoveAndRetrieveIndexerArgumentsNodeReplacement(methodAfterIndexer); public static RenameAndNegateLambdaNodeReplacement RenameAndNegateLambda(string oldName, string newName) => new RenameAndNegateLambdaNodeReplacement(oldName, newName); public static WithArgumentsNodeReplacement WithArguments(string name, SeparatedSyntaxList arguments) => new WithArgumentsNodeReplacement(name, arguments); + public static NodeReplacement AddTypeArgument(string name, TypeSyntax type) => new AddTypeArgumentNodeReplacement(name, type); } } diff --git a/src/FluentAssertions.Analyzers/Utilities/SemanticModelTypeExtensions.cs b/src/FluentAssertions.Analyzers/Utilities/SemanticModelTypeExtensions.cs index 6d9c529d..10d76fea 100644 --- a/src/FluentAssertions.Analyzers/Utilities/SemanticModelTypeExtensions.cs +++ b/src/FluentAssertions.Analyzers/Utilities/SemanticModelTypeExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; namespace FluentAssertions.Analyzers.Utilities { @@ -10,6 +11,21 @@ internal static class SemanticModelTypeExtensions public static INamedTypeSymbol GetActionType(this SemanticModel semanticModel) => GetTypeFrom(semanticModel, typeof(Action)); + public static INamedTypeSymbol GetFloatType(this SemanticModel semanticModel) + => GetTypeFrom(semanticModel, typeof(float)); + + public static INamedTypeSymbol GetDoubleType(this SemanticModel semanticModel) + => GetTypeFrom(semanticModel, SpecialType.System_Double); + + public static INamedTypeSymbol GetStringType(this SemanticModel semanticModel) + => GetTypeFrom(semanticModel, SpecialType.System_String); + + public static INamedTypeSymbol GetCultureInfoType(this SemanticModel semanticModel) + => GetTypeFrom(semanticModel, typeof(CultureInfo)); + + public static INamedTypeSymbol GetBooleanType(this SemanticModel semanticModel) + => GetTypeFrom(semanticModel, SpecialType.System_Boolean); + public static INamedTypeSymbol GetGenericIEnumerableType(this SemanticModel semanticModel) => GetTypeFrom(semanticModel, SpecialType.System_Collections_Generic_IEnumerable_T); diff --git a/src/FluentAssertions.Analyzers/Utilities/TypesExtensions.cs b/src/FluentAssertions.Analyzers/Utilities/TypesExtensions.cs index 08c84e28..ebd88aef 100644 --- a/src/FluentAssertions.Analyzers/Utilities/TypesExtensions.cs +++ b/src/FluentAssertions.Analyzers/Utilities/TypesExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis; -using System; using System.Linq; namespace FluentAssertions.Analyzers.Utilities