Skip to content

Commit 9243578

Browse files
authored
feat: support collection analyzers for arrays (#209)
* add tests for array * add support for arrays in collection analyzers
1 parent 5a81bb4 commit 9243578

File tree

13 files changed

+112
-15
lines changed

13 files changed

+112
-15
lines changed

src/FluentAssertions.Analyzers.Tests/GenerateCode.cs

+28
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ namespace FluentAssertions.Analyzers.Tests
55
{
66
public static class GenerateCode
77
{
8+
public static string GenericArrayCodeBlockAssertion(string assertion) => GenericArrayAssertion(
9+
" {" + Environment.NewLine +
10+
" " + assertion + Environment.NewLine +
11+
" }");
12+
public static string GenericArrayExpressionBodyAssertion(string assertion) => GenericArrayAssertion(
13+
" => " + assertion);
14+
15+
private static string GenericArrayAssertion(string bodyExpression) => new StringBuilder()
16+
.AppendLine("using System.Collections.Generic;")
17+
.AppendLine("using System.Linq;")
18+
.AppendLine("using System;")
19+
.AppendLine("using FluentAssertions;using FluentAssertions.Extensions;")
20+
.AppendLine("namespace TestNamespace")
21+
.AppendLine("{")
22+
.AppendLine(" public class TestClass")
23+
.AppendLine(" {")
24+
.AppendLine(" public void TestMethod(TestComplexClass[] actual, TestComplexClass[] expected, TestComplexClass[] unexpected, TestComplexClass expectedItem, TestComplexClass unexpectedItem, int k)")
25+
.AppendLine(bodyExpression)
26+
.AppendLine(" }")
27+
.AppendLine(" public class TestComplexClass")
28+
.AppendLine(" {")
29+
.AppendLine(" public bool BooleanProperty { get; set; }")
30+
.AppendLine(" public string Message { get; set; }")
31+
.AppendLine(" }")
32+
.AppendMainMethod()
33+
.AppendLine("}")
34+
.ToString();
35+
836
public static string GenericIListCodeBlockAssertion(string assertion) => GenericIListAssertion(
937
" {" + Environment.NewLine +
1038
" " + assertion + Environment.NewLine +

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

+46
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ public class CollectionTests
3838
[Implemented]
3939
public void CollectionsShouldNotBeEmpty_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFixCodeBlock<CollectionShouldNotBeEmptyCodeFix, CollectionShouldNotBeEmptyAnalyzer>(oldAssertion, newAssertion);
4040

41+
[AssertionDataTestMethod]
42+
[AssertionDiagnostic("actual.Any().Should().BeTrue({0});")]
43+
[AssertionDiagnostic("actual.AsEnumerable().Any().Should().BeTrue({0}).And.ToString();")]
44+
[Implemented]
45+
public void CollectionsShouldNotBeEmpty_Array_TestAnalyzer(string assertion) => VerifyArrayCSharpDiagnosticCodeBlock<CollectionShouldNotBeEmptyAnalyzer>(assertion);
46+
47+
[AssertionDataTestMethod]
48+
[AssertionCodeFix(
49+
oldAssertion: "actual.Any().Should().BeTrue({0});",
50+
newAssertion: "actual.Should().NotBeEmpty({0});")]
51+
[AssertionCodeFix(
52+
oldAssertion: "actual.AsEnumerable().Any().Should().BeTrue({0}).And.ToString();",
53+
newAssertion: "actual.AsEnumerable().Should().NotBeEmpty({0}).And.ToString();")]
54+
[Implemented]
55+
public void CollectionsShouldNotBeEmpty_Array_TestCodeFix(string oldAssertion, string newAssertion) => VerifyArrayCSharpFixCodeBlock<CollectionShouldNotBeEmptyCodeFix, CollectionShouldNotBeEmptyAnalyzer>(oldAssertion, newAssertion);
56+
4157
[AssertionDataTestMethod]
4258
[AssertionDiagnostic("actual.Any().Should().BeFalse({0});")]
4359
[AssertionDiagnostic("actual.Should().HaveCount(0{0});")]
@@ -666,6 +682,36 @@ public void CollectionShouldContainSingle_TestAnalyzer_GenericIEnumerableShouldR
666682
});
667683
}
668684

685+
private void VerifyArrayCSharpDiagnosticCodeBlock<TDiagnosticAnalyzer>(string sourceAssertion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
686+
{
687+
var source = GenerateCode.GenericArrayCodeBlockAssertion(sourceAssertion);
688+
689+
var type = typeof(TDiagnosticAnalyzer);
690+
var diagnosticId = (string)type.GetField("DiagnosticId").GetValue(null);
691+
var message = (string)type.GetField("Message").GetValue(null);
692+
693+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult
694+
{
695+
Id = diagnosticId,
696+
Message = message,
697+
Locations = new DiagnosticResultLocation[]
698+
{
699+
new DiagnosticResultLocation("Test0.cs", 11,13)
700+
},
701+
Severity = DiagnosticSeverity.Info
702+
});
703+
}
704+
705+
private void VerifyArrayCSharpFixCodeBlock<TCodeFixProvider, TDiagnosticAnalyzer>(string oldSourceAssertion, string newSourceAssertion)
706+
where TCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider, new()
707+
where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
708+
{
709+
var oldSource = GenerateCode.GenericArrayCodeBlockAssertion(oldSourceAssertion);
710+
var newSource = GenerateCode.GenericArrayCodeBlockAssertion(newSourceAssertion);
711+
712+
DiagnosticVerifier.VerifyCSharpFix<TCodeFixProvider, TDiagnosticAnalyzer>(oldSource, newSource);
713+
}
714+
669715
private void VerifyCSharpFixCodeBlock<TCodeFixProvider, TDiagnosticAnalyzer>(string oldSourceAssertion, string newSourceAssertion)
670716
where TCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider, new()
671717
where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()

src/FluentAssertions.Analyzers/Tips/Collections/CollectionAnalyzer.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ namespace FluentAssertions.Analyzers
55
{
66
public abstract class CollectionAnalyzer : FluentAssertionsAnalyzer
77
{
8-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel)
8+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel)
99
{
1010
return type.SpecialType != SpecialType.System_String
1111
&& type.IsTypeOrConstructedFromTypeOrImplementsType(SpecialType.System_Collections_Generic_IEnumerable_T);
1212
}
13+
14+
override protected bool ShouldAnalyzeVariableType(ITypeSymbol type, SemanticModel semanticModel)
15+
{
16+
return type.SpecialType != SpecialType.System_String
17+
&& type.IsTypeOrConstructedFromTypeOrImplementsType(SpecialType.System_Collections_Generic_IEnumerable_T);
18+
}
19+
1320
}
1421
}

src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldContainSingle.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
2727
}
2828
}
2929

30-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel)
30+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel)
3131
{
3232
if (!type.IsTypeOrConstructedFromTypeOrImplementsType(SpecialType.System_Collections_Generic_IEnumerable_T))
3333
{
3434
return false;
3535
}
3636

37-
return base.ShouldAnalyzeVariableType(type, semanticModel);
37+
return base.ShouldAnalyzeVariableNamedType(type, semanticModel);
3838
}
3939

4040
public class WhereShouldHaveCount1SyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor

src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldHaveElementAt.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
2828
}
2929
}
3030

31-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel)
31+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel)
3232
{
3333
var iReadOnlyDictionaryType = semanticModel.GetIReadOnlyDictionaryType();
3434
var iDictionaryType = semanticModel.GetGenericIDictionaryType();
@@ -38,7 +38,7 @@ protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, Semanti
3838
return false;
3939
}
4040

41-
return base.ShouldAnalyzeVariableType(type, semanticModel);
41+
return base.ShouldAnalyzeVariableNamedType(type, semanticModel);
4242
}
4343

4444
public class ElementAtIndexShouldBeSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor

src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryAnalyzer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace FluentAssertions.Analyzers
55
{
66
public abstract class DictionaryAnalyzer : FluentAssertionsAnalyzer
77
{
8-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel)
8+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel)
99
{
1010
var iDictionaryType = semanticModel.GetGenericIDictionaryType();
1111
return type.IsTypeOrConstructedFromTypeOrImplementsType(iDictionaryType);

src/FluentAssertions.Analyzers/Tips/Exceptions/ExceptionAnalyzer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace FluentAssertions.Analyzers
55
{
66
public abstract class ExceptionAnalyzer : FluentAssertionsAnalyzer
77
{
8-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel)
8+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel)
99
{
1010
var actionType = semanticModel.GetActionType();
1111
return type.IsTypeOrConstructedFromTypeOrImplementsType(actionType);

src/FluentAssertions.Analyzers/Tips/Numerics/NumericAnalyzer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace FluentAssertions.Analyzers
44
{
55
public abstract class NumericAnalyzer : FluentAssertionsAnalyzer
66
{
7-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel) => true;
7+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel) => true;
88
}
99
}

src/FluentAssertions.Analyzers/Tips/Numerics/NumericShouldBeApproximately.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
2929
}
3030

3131
private static readonly string[] ValidaTypeNames = { "double", "decimal", "float" };
32-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel)
32+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel)
3333
=> ValidaTypeNames.Contains(type.ToDisplayString(), StringComparer.OrdinalIgnoreCase);
3434

3535
public class MathAbsShouldBeLessOrEqualToSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor

src/FluentAssertions.Analyzers/Tips/Strings/StringAnalyzer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace FluentAssertions.Analyzers
44
{
55
public abstract class StringAnalyzer : FluentAssertionsAnalyzer
66
{
7-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel) => type.SpecialType == SpecialType.System_String;
7+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel) => type.SpecialType == SpecialType.System_String;
88
}
99
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public abstract class XunitAnalyzer : TestingLibraryAnalyzerBase
99
protected override string TestingLibraryModule => "xunit.assert";
1010
protected override string TestingLibraryAssertionType => "Assert";
1111

12-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel) => type.Name == "Assert";
12+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel) => type.Name == "Assert";
1313
}
1414

1515
public abstract class XunitCodeFixProvider : TestingLibraryCodeFixBase

src/FluentAssertions.Analyzers/Utilities/FluentAssertionsAnalyzer.cs

+14-4
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,18 @@ private void AnalyzeExpressionStatementSyntax(SyntaxNodeAnalysisContext context)
4444
}
4545
}
4646

47-
protected virtual bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel) => true;
47+
protected virtual bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel) => true;
48+
protected virtual bool ShouldAnalyzeVariableType(ITypeSymbol type, SemanticModel semanticModel) => true;
49+
50+
private bool ShouldAnalyzeVariableTypeCore(ITypeSymbol type, SemanticModel semanticModel)
51+
{
52+
if (type is INamedTypeSymbol namedType)
53+
{
54+
return ShouldAnalyzeVariableNamedType(namedType, semanticModel);
55+
}
56+
57+
return ShouldAnalyzeVariableType(type, semanticModel);
58+
}
4859

4960
protected virtual Diagnostic AnalyzeExpression(ExpressionSyntax expression, SemanticModel semanticModel)
5061
{
@@ -53,8 +64,7 @@ protected virtual Diagnostic AnalyzeExpression(ExpressionSyntax expression, Sema
5364

5465
if (variableNameExtractor.VariableIdentifierName == null) return null;
5566
var typeInfo = semanticModel.GetTypeInfo(variableNameExtractor.VariableIdentifierName);
56-
if (!(typeInfo.Type is INamedTypeSymbol namedType)) return null;
57-
if (!ShouldAnalyzeVariableType(namedType, semanticModel)) return null;
67+
if (!ShouldAnalyzeVariableTypeCore(typeInfo.Type, semanticModel)) return null;
5868

5969
foreach (var visitor in Visitors)
6070
{
@@ -104,7 +114,7 @@ public abstract class TestingLibraryAnalyzerBase : FluentAssertionsAnalyzer
104114
protected abstract string TestingLibraryModule { get; }
105115
protected abstract string TestingLibraryAssertionType { get; }
106116

107-
protected override bool ShouldAnalyzeVariableType(INamedTypeSymbol type, SemanticModel semanticModel)
117+
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel)
108118
=> type.Name == TestingLibraryAssertionType && type.ContainingModule.Name == TestingLibraryModule + ".dll";
109119
}
110120
}

src/FluentAssertions.Analyzers/Utilities/TypesExtensions.cs

+6
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,11 @@ public static bool IsTypeOrConstructedFromTypeOrImplementsType(this INamedTypeSy
1818
return abstractType.Equals(other, SymbolEqualityComparer.Default)
1919
|| abstractType.AllInterfaces.Any(@interface => @interface.OriginalDefinition.Equals(other, SymbolEqualityComparer.Default));
2020
}
21+
22+
public static bool IsTypeOrConstructedFromTypeOrImplementsType(this ITypeSymbol type, SpecialType specialType)
23+
{
24+
return type.SpecialType == specialType
25+
|| type.AllInterfaces.Any(@interface => @interface.OriginalDefinition.SpecialType == specialType);
26+
}
2127
}
2228
}

0 commit comments

Comments
 (0)