diff --git a/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs b/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs index 3414de25..787adf34 100644 --- a/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs +++ b/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs @@ -540,6 +540,20 @@ public void AssertStartsWith_TestAnalyzer(string assertion) => public void AssertStartsWith_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("string actual, string expected", oldAssertion, newAssertion); + [DataTestMethod] + [DataRow("Assert.Subset(expected, actual);")] + [Implemented] + public void AssertSubset_TestAnalyzer(string assertion) => + VerifyCSharpDiagnostic("ISet actual, ISet expected", assertion); + + [DataTestMethod] + [DataRow( + /* oldAssertion: */ "Assert.Subset(expected, actual);", + /* newAssertion: */ "actual.Should().BeSubsetOf(expected);")] + [Implemented] + public void AssertSubset_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("ISet actual, ISet 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 916b327d..261c4e1f 100644 --- a/src/FluentAssertions.Analyzers/Constants.cs +++ b/src/FluentAssertions.Analyzers/Constants.cs @@ -136,6 +136,7 @@ public static class Xunit public const string AssertEmpty = $"{DiagnosticProperties.IdPrefix}0714"; public const string AssertEndsWith = $"{DiagnosticProperties.IdPrefix}0715"; public const string AssertStartsWith = $"{DiagnosticProperties.IdPrefix}0716"; + public const string AssertSubset = $"{DiagnosticProperties.IdPrefix}0717"; } } diff --git a/src/FluentAssertions.Analyzers/Tips/Xunit/AssertSubset.cs b/src/FluentAssertions.Analyzers/Tips/Xunit/AssertSubset.cs new file mode 100644 index 00000000..13ec8b64 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/Xunit/AssertSubset.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using FluentAssertions.Analyzers.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace FluentAssertions.Analyzers.Xunit; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class AssertSubsetAnalyzer : XunitAnalyzer +{ + public const string DiagnosticId = Constants.Tips.Xunit.AssertSubset; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().BeSubset()"; + + protected override DiagnosticDescriptor Rule => new(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + + protected override IEnumerable Visitors => new FluentAssertionsCSharpSyntaxVisitor[] + { + new AssertSubsetSyntaxVisitor() + }; + + //public static void Subset(ISet expectedSubset, ISet? actual) + public class AssertSubsetSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertSubsetSyntaxVisitor() : base( + MemberValidator.ArgumentsMatch("Subset", + ArgumentValidator.Exists(), + ArgumentValidator.IsTypeOrConstructedFromTypeOrImplementsType(SpecialType.System_Collections_IEnumerable)) + ) + { + } + } +} + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertSubsetCodeFix)), Shared] +public class AssertSubsetCodeFix : XunitCodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertSubsetAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression( + ExpressionSyntax expression, + FluentAssertionsDiagnosticProperties properties) + { + switch (properties.VisitorName) + { + case nameof(AssertSubsetAnalyzer.AssertSubsetSyntaxVisitor): + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "Subset", "BeSubsetOf"); + default: + throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); + } + } +} \ No newline at end of file