Skip to content

Commit 8891aec

Browse files
authored
Merge pull request #22 from fluentassertions/feature/null-conditional-assertion
[Feature] null conditional assertion
2 parents fd1372e + 78c8560 commit 8891aec

File tree

4 files changed

+142
-2
lines changed

4 files changed

+142
-2
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using FluentAssertions.Analyzers.Tips;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using System.Text;
4+
5+
namespace FluentAssertions.Analyzers.Tests.Tips
6+
{
7+
[TestClass]
8+
public class NullConditionalAssertionTests
9+
{
10+
[AssertionDataTestMethod]
11+
[AssertionDiagnostic("actual?.Should().Be(expected{0});")]
12+
[Implemented]
13+
public void TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion);
14+
15+
private void VerifyCSharpDiagnostic(string assertion)
16+
{
17+
var code = new StringBuilder()
18+
.AppendLine("using System;")
19+
.AppendLine("using FluentAssertions;")
20+
.AppendLine("namespace TestNamespace")
21+
.AppendLine("{")
22+
.AppendLine(" class TestClass")
23+
.AppendLine(" {")
24+
.AppendLine(" void TestMethod(int? actual, int expected)")
25+
.AppendLine(" {")
26+
.AppendLine($" {assertion}")
27+
.AppendLine(" }")
28+
.AppendLine(" }")
29+
.AppendLine(" class Program")
30+
.AppendLine(" {")
31+
.AppendLine(" public static void Main()")
32+
.AppendLine(" {")
33+
.AppendLine(" }")
34+
.AppendLine(" }")
35+
.AppendLine("}")
36+
.ToString();
37+
38+
DiagnosticVerifier.VerifyCSharpDiagnostic<NullConditionalAssertionAnalyzer>(code, new DiagnosticResult
39+
{
40+
Id = NullConditionalAssertionAnalyzer.DiagnosticId,
41+
Message = NullConditionalAssertionAnalyzer.Message,
42+
Severity = Microsoft.CodeAnalysis.DiagnosticSeverity.Warning,
43+
Locations = new DiagnosticResultLocation[]
44+
{
45+
new DiagnosticResultLocation("Test0.cs", 9, 13)
46+
}
47+
});
48+
}
49+
}
50+
}

src/FluentAssertions.Analyzers/Constants.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ public static class Dictionaries
5858
public const string DictionaryShouldNotContainKey = nameof(DictionaryShouldNotContainKey);
5959
public const string DictionaryShouldNotContainValue = nameof(DictionaryShouldNotContainValue);
6060

61-
}
61+
}
62+
}
63+
public static class CodeSmell
64+
{
65+
public const string Category = "FluentAssertionCodeSmell";
66+
67+
public const string NullConditionalAssertion = nameof(NullConditionalAssertion);
6268
}
6369
}
6470
}

src/FluentAssertions.Analyzers/FluentAssertions.Analyzers.nuspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<metadata>
44
<id>FluentAssertions.Analyzers</id>
55
<title>Fluent Assertions Analyzers</title>
6-
<version>0.7.0</version>
6+
<version>0.8.0</version>
77
<owners>Meir Blachman</owners>
88
<authors>Meir Blachman</authors>
99
<summary>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using System.Collections.Immutable;
6+
using System.Linq;
7+
8+
namespace FluentAssertions.Analyzers.Tips
9+
{
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
public class NullConditionalAssertionAnalyzer : DiagnosticAnalyzer
12+
{
13+
public const string DiagnosticId = Constants.CodeSmell.NullConditionalAssertion;
14+
public const string Title = "Code Smell";
15+
public const string Message = "The assertions might not be executed when using ?.Should()";
16+
17+
public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, Message, Constants.CodeSmell.Category, DiagnosticSeverity.Warning, true);
18+
19+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
20+
21+
public sealed override void Initialize(AnalysisContext context)
22+
{
23+
context.EnableConcurrentExecution();
24+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
25+
context.RegisterCodeBlockAction(AnalyzeCodeBlock);
26+
}
27+
28+
private void AnalyzeCodeBlock(CodeBlockAnalysisContext context)
29+
{
30+
var method = context.CodeBlock as MethodDeclarationSyntax;
31+
if (method == null) return;
32+
33+
if (method.Body != null)
34+
{
35+
foreach (var statement in method.Body.Statements.OfType<ExpressionStatementSyntax>())
36+
{
37+
var diagnostic = AnalyzeExpression(statement.Expression);
38+
if (diagnostic != null)
39+
{
40+
context.ReportDiagnostic(diagnostic);
41+
}
42+
}
43+
return;
44+
}
45+
if (method.ExpressionBody != null)
46+
{
47+
var diagnostic = AnalyzeExpression(method.ExpressionBody.Expression);
48+
if (diagnostic != null)
49+
{
50+
context.ReportDiagnostic(diagnostic);
51+
}
52+
}
53+
}
54+
55+
protected virtual Diagnostic AnalyzeExpression(ExpressionSyntax expression)
56+
{
57+
var visitor = new ConditionalAccessExpressionVisitor();
58+
expression.Accept(visitor);
59+
60+
return visitor.CodeSmells ? Diagnostic.Create(descriptor: Rule, location: expression.GetLocation()) : null;
61+
}
62+
63+
private class ConditionalAccessExpressionVisitor : CSharpSyntaxWalker
64+
{
65+
public bool CodeSmells { get; private set; }
66+
public Location ConditionalAccess { get; private set; }
67+
68+
public override void VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node)
69+
{
70+
if (CodeSmells) return;
71+
72+
CodeSmells = node.WhenNotNull is InvocationExpressionSyntax invocation
73+
&& invocation.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.IsKind(SyntaxKind.SimpleMemberAccessExpression)
74+
&& memberAccess.Expression is InvocationExpressionSyntax shouldInvocation
75+
&& shouldInvocation.Expression is MemberBindingExpressionSyntax memberBinding
76+
&& memberBinding.Name.Identifier.ValueText == "Should";
77+
if (CodeSmells)
78+
{
79+
ConditionalAccess = node.GetLocation();
80+
}
81+
}
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)