diff --git a/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs b/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs index ce8a732b..eac0291c 100644 --- a/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs +++ b/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs @@ -340,6 +340,20 @@ public void AssertNull_TestCodeFix(string oldAssertion, string newAssertion) public void AssertNotNull_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("object actual", oldAssertion, newAssertion); + [DataTestMethod] + [DataRow("Assert.Contains(expected, actual);")] + [Implemented] + public void AssertStringContains_TestAnalyzer(string assertion) => + VerifyCSharpDiagnostic("string actual, string expected", assertion); + + [DataTestMethod] + [DataRow( + /* oldAssertion: */ "Assert.Contains(expected, actual);", + /* newAssertion: */ "actual.Should().Contain(expected);")] + [Implemented] + public void AssertStringContains_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("string actual, string expected", oldAssertion, newAssertion); + private void VerifyCSharpDiagnostic(string methodArguments, string assertion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new() { var source = GenerateCode.XunitAssertion(methodArguments, assertion); diff --git a/src/FluentAssertions.Analyzers/Constants.cs b/src/FluentAssertions.Analyzers/Constants.cs index 82e065e0..6c72555c 100644 --- a/src/FluentAssertions.Analyzers/Constants.cs +++ b/src/FluentAssertions.Analyzers/Constants.cs @@ -129,6 +129,7 @@ public static class Xunit public const string AssertNotStrictEqual = $"{DiagnosticProperties.IdPrefix}0707"; public const string AssertNull = $"{DiagnosticProperties.IdPrefix}0708"; public const string AssertNotNull = $"{DiagnosticProperties.IdPrefix}0709"; + public const string AssertContains = $"{DiagnosticProperties.IdPrefix}0710"; } } diff --git a/src/FluentAssertions.Analyzers/Tips/Xunit/AssertContains.cs b/src/FluentAssertions.Analyzers/Tips/Xunit/AssertContains.cs new file mode 100644 index 00000000..bebea9bb --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/Xunit/AssertContains.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using TypeSelector = FluentAssertions.Analyzers.Utilities.SemanticModelTypeExtensions; + +namespace FluentAssertions.Analyzers.Xunit; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class AssertContainsAnalyzer : XunitAnalyzer +{ + public const string DiagnosticId = Constants.Tips.Xunit.AssertContains; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().Contain()."; + + protected override DiagnosticDescriptor Rule => new(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + + protected override IEnumerable Visitors => new FluentAssertionsCSharpSyntaxVisitor[] + { + new AssertContainsStringSyntaxVisitor() + }; + + //public static void Contains(string expectedSubstring, string? actualString) + public class AssertContainsStringSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertContainsStringSyntaxVisitor() : base( + MemberValidator.ArgumentsMatch("Contains", + ArgumentValidator.IsType(TypeSelector.GetStringType), + ArgumentValidator.IsType(TypeSelector.GetStringType)) + ) + { + } + } +} + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertContainsCodeFix)), Shared] +public class AssertContainsCodeFix : XunitCodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertContainsAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression( + ExpressionSyntax expression, + FluentAssertionsDiagnosticProperties properties) + { + switch (properties.VisitorName) + { + case nameof(AssertContainsAnalyzer.AssertContainsStringSyntaxVisitor): + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "Contains", "Contain"); + default: + throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); + } + } +} \ No newline at end of file