From b92d2f5a0c075fa74bc0aab34334f1e37d3294f6 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Tue, 10 Oct 2023 17:10:25 +0300 Subject: [PATCH] add support for xunit TypeAsserts IsNotAssignableFrom --- .../Tips/XunitTests.cs | 22 ++++++ src/FluentAssertions.Analyzers/Constants.cs | 1 + .../Tips/MsTest/AssertIsInstanceOfType.cs | 20 +----- .../Tips/MsTest/AssertIsNotInstanceOfType.cs | 20 +----- .../Tips/Xunit/AssertIsAssignableFrom.cs | 20 +----- .../Tips/Xunit/AssertIsNotAssignableFrom.cs | 71 +++++++++++++++++++ .../Utilities/TestingLibraryCodeFixBase.cs | 25 +++++++ 7 files changed, 122 insertions(+), 57 deletions(-) create mode 100644 src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsNotAssignableFrom.cs diff --git a/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs b/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs index 295190b3..b534f7fc 100644 --- a/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs +++ b/src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs @@ -576,6 +576,28 @@ public void AssertIsAssignableFrom_TestAnalyzer(string assertion) => public void AssertIsAssignableFrom_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix("string actual, Type expected", oldAssertion, newAssertion); + [DataTestMethod] + [DataRow("Assert.IsNotAssignableFrom(expected, actual);")] + [DataRow("Assert.IsNotAssignableFrom(typeof(string), actual);")] + [DataRow("Assert.IsNotAssignableFrom(actual);")] + [Implemented] + public void AssertIsNotAssignableFrom_TestAnalyzer(string assertion) => + VerifyCSharpDiagnostic("string actual, Type expected", assertion); + + [DataTestMethod] + [DataRow( + /* oldAssertion: */ "Assert.IsNotAssignableFrom(expected, actual);", + /* newAssertion: */ "actual.Should().NotBeAssignableTo(expected);")] + [DataRow( + /* oldAssertion: */ "Assert.IsNotAssignableFrom(typeof(string), actual);", + /* newAssertion: */ "actual.Should().NotBeAssignableTo();")] + [DataRow( + /* oldAssertion: */ "Assert.IsNotAssignableFrom(actual);", + /* newAssertion: */ "actual.Should().NotBeAssignableTo();")] + [Implemented] + public void AssertIsNotAssignableFrom_TestCodeFix(string oldAssertion, string newAssertion) + => VerifyCSharpFix("string actual, Type 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 617c3b4c..1e8e8e29 100644 --- a/src/FluentAssertions.Analyzers/Constants.cs +++ b/src/FluentAssertions.Analyzers/Constants.cs @@ -138,6 +138,7 @@ public static class Xunit public const string AssertStartsWith = $"{DiagnosticProperties.IdPrefix}0716"; public const string AssertSubset = $"{DiagnosticProperties.IdPrefix}0717"; public const string AssertIsAssignableFrom = $"{DiagnosticProperties.IdPrefix}0718"; + public const string AssertIsNotAssignableFrom = $"{DiagnosticProperties.IdPrefix}0719"; } } diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsInstanceOfType.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsInstanceOfType.cs index 452e3c38..607fa7bc 100644 --- a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsInstanceOfType.cs +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsInstanceOfType.cs @@ -43,24 +43,6 @@ public class AssertIsInstanceOfTypeCodeFix : MsTestAssertCodeFixProvider protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) { var newExpression = RenameMethodAndReplaceWithSubjectShould(expression, "IsInstanceOfType", "BeOfType"); - - 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; + return ReplaceTypeOfArgumentWithGenericTypeIfExists(newExpression, "BeOfType"); } } \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotInstanceOfType.cs b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotInstanceOfType.cs index b77e369a..354e78f0 100644 --- a/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotInstanceOfType.cs +++ b/src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotInstanceOfType.cs @@ -43,24 +43,6 @@ public class AssertIsNotInstanceOfTypeCodeFix : MsTestAssertCodeFixProvider protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) { var newExpression = RenameMethodAndReplaceWithSubjectShould(expression, "IsNotInstanceOfType", "NotBeOfType"); - - 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; + return ReplaceTypeOfArgumentWithGenericTypeIfExists(newExpression, "NotBeOfType"); } } \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsAssignableFrom.cs b/src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsAssignableFrom.cs index 69917a89..c136e28c 100644 --- a/src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsAssignableFrom.cs +++ b/src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsAssignableFrom.cs @@ -63,25 +63,7 @@ protected override ExpressionSyntax GetNewExpression( return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsAssignableFrom", "BeAssignableTo"); case nameof(AssertIsAssignableFromAnalyzer.AssertIsAssignableFromTypeSyntaxVisitor): var newExpression = RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsAssignableFrom", "BeAssignableTo"); - - var beAssignableTo = newExpression.DescendantNodes() - .OfType() - .First(node => node.Name.Identifier.Text == "BeAssignableTo"); - - if (beAssignableTo.Parent is InvocationExpressionSyntax invocation) - { - var arguments = invocation.ArgumentList.Arguments; - if (arguments.Any() && arguments[0].Expression is TypeOfExpressionSyntax typeOfExpression) - { - var genericBeOfType = beAssignableTo.WithName(SF.GenericName(beAssignableTo.Name.Identifier.Text) - .AddTypeArgumentListArguments(typeOfExpression.Type) - ); - newExpression = newExpression.ReplaceNode(beAssignableTo, genericBeOfType); - return GetNewExpression(newExpression, NodeReplacement.RemoveFirstArgument("BeAssignableTo")); - } - } - - return newExpression; + return ReplaceTypeOfArgumentWithGenericTypeIfExists(newExpression, "BeAssignableTo"); default: throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); } diff --git a/src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsNotAssignableFrom.cs b/src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsNotAssignableFrom.cs new file mode 100644 index 00000000..425fbeff --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsNotAssignableFrom.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using FluentAssertions.Analyzers.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace FluentAssertions.Analyzers.Xunit; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class AssertIsNotAssignableFromAnalyzer : XunitAnalyzer +{ + public const string DiagnosticId = Constants.Tips.Xunit.AssertIsNotAssignableFrom; + public const string Category = Constants.Tips.Category; + + public const string Message = "Use .Should().NotBeAssignableTo()."; + + protected override DiagnosticDescriptor Rule => new(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); + + protected override IEnumerable Visitors => new FluentAssertionsCSharpSyntaxVisitor[] + { + new AssertIsNotAssignableFromGenericTypeSyntaxVisitor(), + new AssertIsNotAssignableFromTypeSyntaxVisitor() + }; + + //public static T IsNotAssignableFrom(object? @object) + public class AssertIsNotAssignableFromGenericTypeSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertIsNotAssignableFromGenericTypeSyntaxVisitor() : base( + MemberValidator.HasArguments("IsNotAssignableFrom", 1) + ) + { + } + } + + //public static T IsNotAssignableFrom(Type expectedType, object? @object) + public class AssertIsNotAssignableFromTypeSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor + { + public AssertIsNotAssignableFromTypeSyntaxVisitor() : base( + MemberValidator.HasArguments("IsNotAssignableFrom", 2) + ) + { + } + } +} + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertIsNotAssignableFromCodeFix)), Shared] +public class AssertIsNotAssignableFromCodeFix : XunitCodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertIsNotAssignableFromAnalyzer.DiagnosticId); + + protected override ExpressionSyntax GetNewExpression( + ExpressionSyntax expression, + FluentAssertionsDiagnosticProperties properties) + { + switch (properties.VisitorName) + { + case nameof(AssertIsNotAssignableFromAnalyzer.AssertIsNotAssignableFromGenericTypeSyntaxVisitor): + return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsNotAssignableFrom", "NotBeAssignableTo"); + case nameof(AssertIsNotAssignableFromAnalyzer.AssertIsNotAssignableFromTypeSyntaxVisitor): + var newExpression = RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsNotAssignableFrom", "NotBeAssignableTo"); + return ReplaceTypeOfArgumentWithGenericTypeIfExists(newExpression, "NotBeAssignableTo"); + default: + throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); + } + } +} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Utilities/TestingLibraryCodeFixBase.cs b/src/FluentAssertions.Analyzers/Utilities/TestingLibraryCodeFixBase.cs index 88b2ab92..eb2fec11 100644 --- a/src/FluentAssertions.Analyzers/Utilities/TestingLibraryCodeFixBase.cs +++ b/src/FluentAssertions.Analyzers/Utilities/TestingLibraryCodeFixBase.cs @@ -1,4 +1,7 @@ +using System.Linq; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace FluentAssertions.Analyzers.Utilities; @@ -25,4 +28,26 @@ protected ExpressionSyntax RenameMethodAndReorderActualExpectedAndReplaceWithSub return GetNewExpression(newExpression, NodeReplacement.WithArguments(newName, rename.Arguments.RemoveAt(1))); } + + protected ExpressionSyntax ReplaceTypeOfArgumentWithGenericTypeIfExists(ExpressionSyntax expression, string method) + { + var methodExpression = expression.DescendantNodes() + .OfType() + .First(node => node.Name.Identifier.Text == method); + + if (methodExpression.Parent is InvocationExpressionSyntax invocation) + { + var arguments = invocation.ArgumentList.Arguments; + if (arguments.Any() && arguments[0].Expression is TypeOfExpressionSyntax typeOfExpression) + { + var genericBeOfType = methodExpression.WithName(SF.GenericName(methodExpression.Name.Identifier.Text) + .AddTypeArgumentListArguments(typeOfExpression.Type) + ); + var newExpression = expression.ReplaceNode(methodExpression, genericBeOfType); + return GetNewExpression(newExpression, NodeReplacement.RemoveFirstArgument(method)); + } + } + + return expression; + } }