Skip to content

Commit 06b2dea

Browse files
Fix NullConditionalAssertion false positive (#170)
* Fix null conditional assertion tip * Specify more testcases * Ignore null conditional inside ArgumentList, but current implementation creates false negatives. * Add boolean value for each scope level where relevant. * Ordering * Remove unused * Visit ParenthesizedExpressionSyntax not necessary as ConditionalAccessExpressionSyntax already includes this. * Rename variable * Rename again
1 parent b1b70b6 commit 06b2dea

File tree

2 files changed

+44
-19
lines changed

2 files changed

+44
-19
lines changed

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using Microsoft.VisualStudio.TestTools.UnitTesting;
22
using System.Text;
33

4-
namespace FluentAssertions.Analyzers.Tests
4+
namespace FluentAssertions.Analyzers.Tests.Tips
55
{
66
[TestClass]
77
public class NullConditionalAssertionTests
@@ -10,13 +10,25 @@ public class NullConditionalAssertionTests
1010
[AssertionDiagnostic("actual?.Should().Be(expected{0});")]
1111
[AssertionDiagnostic("actual?.MyProperty.Should().Be(\"test\"{0});")]
1212
[AssertionDiagnostic("actual.MyProperty?.Should().Be(\"test\"{0});")]
13+
[AssertionDiagnostic("(actual.MyProperty)?.Should().Be(\"test\"{0});")]
14+
[AssertionDiagnostic("(actual?.MyProperty)?.Should().Be(\"test\"{0});")]
15+
[AssertionDiagnostic("actual?.MyProperty.Should().Be(actual?.MyProperty{0});")]
16+
[AssertionDiagnostic("actual.MyList?.Where(obj => obj?.ToString() == null).Count().Should().Be(0{0});")]
1317
[Implemented]
14-
public void TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion);
18+
public void NullConditionalMayNotExecuteTest(string assertion) => VerifyCSharpDiagnostic(assertion);
1519

16-
private void VerifyCSharpDiagnostic(string assertion)
17-
{
18-
var code = new StringBuilder()
20+
[AssertionDataTestMethod]
21+
[AssertionDiagnostic("(actual?.MyProperty).Should().Be(\"test\"{0});")]
22+
[AssertionDiagnostic("actual.MyProperty.Should().Be(actual?.MyProperty{0});")]
23+
[AssertionDiagnostic("actual.MyList.Where(obj => obj?.ToString() == null).Count().Should().Be(0{0});")]
24+
[Implemented]
25+
public void NullConditionalWillStillExecuteTest(string assertion) => VerifyCSharpDiagnosticPass(assertion);
26+
27+
private static string Code(string assertion) =>
28+
new StringBuilder()
1929
.AppendLine("using System;")
30+
.AppendLine("using System.Collections.Generic;")
31+
.AppendLine("using System.Linq;")
2032
.AppendLine("using FluentAssertions;using FluentAssertions.Extensions;")
2133
.AppendLine("namespace TestNamespace")
2234
.AppendLine("{")
@@ -30,6 +42,7 @@ private void VerifyCSharpDiagnostic(string assertion)
3042
.AppendLine(" class MyClass")
3143
.AppendLine(" {")
3244
.AppendLine(" public string MyProperty { get; set; }")
45+
.AppendLine(" public List<object> MyList { get; set; }")
3346
.AppendLine(" }")
3447
.AppendLine(" class Program")
3548
.AppendLine(" {")
@@ -40,16 +53,19 @@ private void VerifyCSharpDiagnostic(string assertion)
4053
.AppendLine("}")
4154
.ToString();
4255

43-
DiagnosticVerifier.VerifyCSharpDiagnostic<NullConditionalAssertionAnalyzer>(code, new DiagnosticResult
56+
private static void VerifyCSharpDiagnosticPass(string assertion)
57+
=> DiagnosticVerifier.VerifyCSharpDiagnostic<NullConditionalAssertionAnalyzer>(Code(assertion));
58+
59+
private static void VerifyCSharpDiagnostic(string assertion)
60+
=> DiagnosticVerifier.VerifyCSharpDiagnostic<NullConditionalAssertionAnalyzer>(Code(assertion), new DiagnosticResult
4461
{
4562
Id = NullConditionalAssertionAnalyzer.DiagnosticId,
4663
Message = NullConditionalAssertionAnalyzer.Message,
4764
Severity = Microsoft.CodeAnalysis.DiagnosticSeverity.Warning,
4865
Locations = new DiagnosticResultLocation[]
4966
{
50-
new DiagnosticResultLocation("Test0.cs", 9, 13)
67+
new DiagnosticResultLocation("Test0.cs", 11, 13)
5168
}
5269
});
53-
}
5470
}
5571
}

src/FluentAssertions.Analyzers/Tips/NullConditionalAssertionAnalyzer.cs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.CodeAnalysis.CSharp;
33
using Microsoft.CodeAnalysis.CSharp.Syntax;
44
using Microsoft.CodeAnalysis.Diagnostics;
5+
using System.Collections.Generic;
56
using System.Collections.Immutable;
67
using System.Linq;
78

@@ -62,25 +63,33 @@ protected virtual Diagnostic AnalyzeExpression(ExpressionSyntax expression)
6263

6364
private class ConditionalAccessExpressionVisitor : CSharpSyntaxWalker
6465
{
65-
private bool _foundConditionalAccess;
66-
private bool _foundShouldMethod;
66+
private readonly Stack<bool> _foundConditionalAccess = new();
67+
private bool _foundShouldMethodAfterConditionalAccessInSameScope;
6768

68-
public bool CodeSmells => _foundShouldMethod && _foundConditionalAccess;
69-
public Location ConditionalAccess { get; private set; }
69+
private bool FoundConditionalAccessInCurrentScope => _foundConditionalAccess.Any() && _foundConditionalAccess.Peek();
70+
71+
public bool CodeSmells => _foundShouldMethodAfterConditionalAccessInSameScope;
7072

7173
public override void VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node)
7274
{
73-
if (!_foundConditionalAccess)
74-
{
75-
_foundConditionalAccess = true;
76-
}
77-
base.VisitConditionalAccessExpression(node);
75+
Visit(node.Expression);
76+
_foundConditionalAccess.Push(true);
77+
Visit(node.WhenNotNull);
78+
_foundConditionalAccess.Pop();
7879
}
80+
81+
public override void VisitArgumentList(ArgumentListSyntax node)
82+
{
83+
_foundConditionalAccess.Push(false);
84+
base.DefaultVisit(node);
85+
_foundConditionalAccess.Pop();
86+
}
87+
7988
public override void VisitIdentifierName(IdentifierNameSyntax node)
8089
{
81-
if(_foundConditionalAccess && node.Identifier.ValueText == "Should")
90+
if (FoundConditionalAccessInCurrentScope && node.Identifier.ValueText == "Should")
8291
{
83-
_foundShouldMethod = true;
92+
_foundShouldMethodAfterConditionalAccessInSameScope = true;
8493
}
8594
}
8695
}

0 commit comments

Comments
 (0)