Skip to content

Commit bf46054

Browse files
authored
add support for xunit TypeAsserts IsNotAssignableFrom (#239)
1 parent 7386e87 commit bf46054

File tree

7 files changed

+122
-57
lines changed

7 files changed

+122
-57
lines changed

src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,28 @@ public void AssertIsAssignableFrom_TestAnalyzer(string assertion) =>
576576
public void AssertIsAssignableFrom_TestCodeFix(string oldAssertion, string newAssertion)
577577
=> VerifyCSharpFix<AssertIsAssignableFromCodeFix, AssertIsAssignableFromAnalyzer>("string actual, Type expected", oldAssertion, newAssertion);
578578

579+
[DataTestMethod]
580+
[DataRow("Assert.IsNotAssignableFrom(expected, actual);")]
581+
[DataRow("Assert.IsNotAssignableFrom(typeof(string), actual);")]
582+
[DataRow("Assert.IsNotAssignableFrom<string>(actual);")]
583+
[Implemented]
584+
public void AssertIsNotAssignableFrom_TestAnalyzer(string assertion) =>
585+
VerifyCSharpDiagnostic<AssertIsNotAssignableFromAnalyzer>("string actual, Type expected", assertion);
586+
587+
[DataTestMethod]
588+
[DataRow(
589+
/* oldAssertion: */ "Assert.IsNotAssignableFrom(expected, actual);",
590+
/* newAssertion: */ "actual.Should().NotBeAssignableTo(expected);")]
591+
[DataRow(
592+
/* oldAssertion: */ "Assert.IsNotAssignableFrom(typeof(string), actual);",
593+
/* newAssertion: */ "actual.Should().NotBeAssignableTo<string>();")]
594+
[DataRow(
595+
/* oldAssertion: */ "Assert.IsNotAssignableFrom<string>(actual);",
596+
/* newAssertion: */ "actual.Should().NotBeAssignableTo<string>();")]
597+
[Implemented]
598+
public void AssertIsNotAssignableFrom_TestCodeFix(string oldAssertion, string newAssertion)
599+
=> VerifyCSharpFix<AssertIsNotAssignableFromCodeFix, AssertIsNotAssignableFromAnalyzer>("string actual, Type expected", oldAssertion, newAssertion);
600+
579601
private void VerifyCSharpDiagnostic<TDiagnosticAnalyzer>(string methodArguments, string assertion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
580602
{
581603
var source = GenerateCode.XunitAssertion(methodArguments, assertion);

src/FluentAssertions.Analyzers/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public static class Xunit
138138
public const string AssertStartsWith = $"{DiagnosticProperties.IdPrefix}0716";
139139
public const string AssertSubset = $"{DiagnosticProperties.IdPrefix}0717";
140140
public const string AssertIsAssignableFrom = $"{DiagnosticProperties.IdPrefix}0718";
141+
public const string AssertIsNotAssignableFrom = $"{DiagnosticProperties.IdPrefix}0719";
141142
}
142143
}
143144

src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsInstanceOfType.cs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,6 @@ public class AssertIsInstanceOfTypeCodeFix : MsTestAssertCodeFixProvider
4343
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
4444
{
4545
var newExpression = RenameMethodAndReplaceWithSubjectShould(expression, "IsInstanceOfType", "BeOfType");
46-
47-
var beOfType = newExpression.DescendantNodes()
48-
.OfType<MemberAccessExpressionSyntax>()
49-
.First(node => node.Name.Identifier.Text == "BeOfType");
50-
51-
if (beOfType.Parent is InvocationExpressionSyntax invocation)
52-
{
53-
var arguments = invocation.ArgumentList.Arguments;
54-
if (arguments.Any() && arguments[0].Expression is TypeOfExpressionSyntax typeOfExpression)
55-
{
56-
var genericBeOfType = beOfType.WithName(SF.GenericName(beOfType.Name.Identifier.Text)
57-
.AddTypeArgumentListArguments(typeOfExpression.Type)
58-
);
59-
newExpression = newExpression.ReplaceNode(beOfType, genericBeOfType);
60-
return GetNewExpression(newExpression, NodeReplacement.RemoveFirstArgument("BeOfType"));
61-
}
62-
}
63-
64-
return newExpression;
46+
return ReplaceTypeOfArgumentWithGenericTypeIfExists(newExpression, "BeOfType");
6547
}
6648
}

src/FluentAssertions.Analyzers/Tips/MsTest/AssertIsNotInstanceOfType.cs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,6 @@ public class AssertIsNotInstanceOfTypeCodeFix : MsTestAssertCodeFixProvider
4343
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
4444
{
4545
var newExpression = RenameMethodAndReplaceWithSubjectShould(expression, "IsNotInstanceOfType", "NotBeOfType");
46-
47-
var beOfType = newExpression.DescendantNodes()
48-
.OfType<MemberAccessExpressionSyntax>()
49-
.First(node => node.Name.Identifier.Text == "NotBeOfType");
50-
51-
if (beOfType.Parent is InvocationExpressionSyntax invocation)
52-
{
53-
var arguments = invocation.ArgumentList.Arguments;
54-
if (arguments.Any() && arguments[0].Expression is TypeOfExpressionSyntax typeOfExpression)
55-
{
56-
var genericBeOfType = beOfType.WithName(SF.GenericName(beOfType.Name.Identifier.Text)
57-
.AddTypeArgumentListArguments(typeOfExpression.Type)
58-
);
59-
newExpression = newExpression.ReplaceNode(beOfType, genericBeOfType);
60-
return GetNewExpression(newExpression, NodeReplacement.RemoveFirstArgument("NotBeOfType"));
61-
}
62-
}
63-
64-
return newExpression;
46+
return ReplaceTypeOfArgumentWithGenericTypeIfExists(newExpression, "NotBeOfType");
6547
}
6648
}

src/FluentAssertions.Analyzers/Tips/Xunit/AssertIsAssignableFrom.cs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,7 @@ protected override ExpressionSyntax GetNewExpression(
6363
return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsAssignableFrom", "BeAssignableTo");
6464
case nameof(AssertIsAssignableFromAnalyzer.AssertIsAssignableFromTypeSyntaxVisitor):
6565
var newExpression = RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsAssignableFrom", "BeAssignableTo");
66-
67-
var beAssignableTo = newExpression.DescendantNodes()
68-
.OfType<MemberAccessExpressionSyntax>()
69-
.First(node => node.Name.Identifier.Text == "BeAssignableTo");
70-
71-
if (beAssignableTo.Parent is InvocationExpressionSyntax invocation)
72-
{
73-
var arguments = invocation.ArgumentList.Arguments;
74-
if (arguments.Any() && arguments[0].Expression is TypeOfExpressionSyntax typeOfExpression)
75-
{
76-
var genericBeOfType = beAssignableTo.WithName(SF.GenericName(beAssignableTo.Name.Identifier.Text)
77-
.AddTypeArgumentListArguments(typeOfExpression.Type)
78-
);
79-
newExpression = newExpression.ReplaceNode(beAssignableTo, genericBeOfType);
80-
return GetNewExpression(newExpression, NodeReplacement.RemoveFirstArgument("BeAssignableTo"));
81-
}
82-
}
83-
84-
return newExpression;
66+
return ReplaceTypeOfArgumentWithGenericTypeIfExists(newExpression, "BeAssignableTo");
8567
default:
8668
throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}");
8769
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System.Collections.Generic;
2+
using System.Collections.Immutable;
3+
using System.Composition;
4+
using System.Linq;
5+
using FluentAssertions.Analyzers.Utilities;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CodeFixes;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
11+
12+
namespace FluentAssertions.Analyzers.Xunit;
13+
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public class AssertIsNotAssignableFromAnalyzer : XunitAnalyzer
16+
{
17+
public const string DiagnosticId = Constants.Tips.Xunit.AssertIsNotAssignableFrom;
18+
public const string Category = Constants.Tips.Category;
19+
20+
public const string Message = "Use .Should().NotBeAssignableTo().";
21+
22+
protected override DiagnosticDescriptor Rule => new(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
23+
24+
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors => new FluentAssertionsCSharpSyntaxVisitor[]
25+
{
26+
new AssertIsNotAssignableFromGenericTypeSyntaxVisitor(),
27+
new AssertIsNotAssignableFromTypeSyntaxVisitor()
28+
};
29+
30+
//public static T IsNotAssignableFrom<T>(object? @object)
31+
public class AssertIsNotAssignableFromGenericTypeSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
32+
{
33+
public AssertIsNotAssignableFromGenericTypeSyntaxVisitor() : base(
34+
MemberValidator.HasArguments("IsNotAssignableFrom", 1)
35+
)
36+
{
37+
}
38+
}
39+
40+
//public static T IsNotAssignableFrom(Type expectedType, object? @object)
41+
public class AssertIsNotAssignableFromTypeSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
42+
{
43+
public AssertIsNotAssignableFromTypeSyntaxVisitor() : base(
44+
MemberValidator.HasArguments("IsNotAssignableFrom", 2)
45+
)
46+
{
47+
}
48+
}
49+
}
50+
51+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertIsNotAssignableFromCodeFix)), Shared]
52+
public class AssertIsNotAssignableFromCodeFix : XunitCodeFixProvider
53+
{
54+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AssertIsNotAssignableFromAnalyzer.DiagnosticId);
55+
56+
protected override ExpressionSyntax GetNewExpression(
57+
ExpressionSyntax expression,
58+
FluentAssertionsDiagnosticProperties properties)
59+
{
60+
switch (properties.VisitorName)
61+
{
62+
case nameof(AssertIsNotAssignableFromAnalyzer.AssertIsNotAssignableFromGenericTypeSyntaxVisitor):
63+
return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsNotAssignableFrom", "NotBeAssignableTo");
64+
case nameof(AssertIsNotAssignableFromAnalyzer.AssertIsNotAssignableFromTypeSyntaxVisitor):
65+
var newExpression = RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "IsNotAssignableFrom", "NotBeAssignableTo");
66+
return ReplaceTypeOfArgumentWithGenericTypeIfExists(newExpression, "NotBeAssignableTo");
67+
default:
68+
throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}");
69+
}
70+
}
71+
}

src/FluentAssertions.Analyzers/Utilities/TestingLibraryCodeFixBase.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System.Linq;
2+
using Microsoft.CodeAnalysis;
13
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
25

36
namespace FluentAssertions.Analyzers.Utilities;
47

@@ -25,4 +28,26 @@ protected ExpressionSyntax RenameMethodAndReorderActualExpectedAndReplaceWithSub
2528

2629
return GetNewExpression(newExpression, NodeReplacement.WithArguments(newName, rename.Arguments.RemoveAt(1)));
2730
}
31+
32+
protected ExpressionSyntax ReplaceTypeOfArgumentWithGenericTypeIfExists(ExpressionSyntax expression, string method)
33+
{
34+
var methodExpression = expression.DescendantNodes()
35+
.OfType<MemberAccessExpressionSyntax>()
36+
.First(node => node.Name.Identifier.Text == method);
37+
38+
if (methodExpression.Parent is InvocationExpressionSyntax invocation)
39+
{
40+
var arguments = invocation.ArgumentList.Arguments;
41+
if (arguments.Any() && arguments[0].Expression is TypeOfExpressionSyntax typeOfExpression)
42+
{
43+
var genericBeOfType = methodExpression.WithName(SF.GenericName(methodExpression.Name.Identifier.Text)
44+
.AddTypeArgumentListArguments(typeOfExpression.Type)
45+
);
46+
var newExpression = expression.ReplaceNode(methodExpression, genericBeOfType);
47+
return GetNewExpression(newExpression, NodeReplacement.RemoveFirstArgument(method));
48+
}
49+
}
50+
51+
return expression;
52+
}
2853
}

0 commit comments

Comments
 (0)