Skip to content

Commit 377f1e3

Browse files
authored
async void analyzer (#72)
* added analyzer for async-void * more progress
1 parent b52fa76 commit 377f1e3

File tree

5 files changed

+168
-2
lines changed

5 files changed

+168
-2
lines changed

src/FluentAssertions.Analyzers.Tests/GenerateCode.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,24 @@ public static string EnumerableExpressionBodyAssertion(string assertion) => Enum
122122
.AppendLine("}")
123123
.ToString();
124124

125+
public static string AsyncFunctionStatement(string statement) => new StringBuilder()
126+
.AppendLine("using System;")
127+
.AppendLine("using System.Threading.Tasks;")
128+
.AppendLine("using FluentAssertions;using FluentAssertions.Extensions;")
129+
.AppendLine("namespace TestNamespace")
130+
.AppendLine("{")
131+
.AppendLine(" class TestClass")
132+
.AppendLine(" {")
133+
.AppendLine(" void TestMethod()")
134+
.AppendLine(" {")
135+
.AppendLine($" {statement}")
136+
.AppendLine(" }")
137+
.AppendLine(" async void AsyncVoidMethod() { await Task.CompletedTask; }")
138+
.AppendLine(" }")
139+
.AppendMainMethod()
140+
.AppendLine("}")
141+
.ToString();
142+
125143
private static StringBuilder AppendMainMethod(this StringBuilder builder) => builder
126144
.AppendLine(" class Program")
127145
.AppendLine(" {")
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
4+
namespace FluentAssertions.Analyzers.Tests
5+
{
6+
[TestClass]
7+
public class AsyncVoidTests
8+
{
9+
[TestMethod]
10+
[Implemented]
11+
public void AssignAsyncVoidMethodToAction_TestAnalyzer()
12+
{
13+
const string statement = "Action action = AsyncVoidMethod;";
14+
var source = GenerateCode.AsyncFunctionStatement(statement);
15+
16+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source);
17+
}
18+
19+
[TestMethod]
20+
[Implemented]
21+
public void AssignVoidLambdaToAction_TestAnalyzer()
22+
{
23+
const string statement = "Action action = () => {};";
24+
var source = GenerateCode.AsyncFunctionStatement(statement);
25+
26+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source);
27+
}
28+
29+
[DataRow("Action action = async () => { await Task.CompletedTask; };")]
30+
[DataRow("Action action1 = async () => { await Task.CompletedTask; }, action2 = async () => { await Task.CompletedTask; };")]
31+
[DataRow("Action action1 = () => { }, action2 = async () => { await Task.CompletedTask; };")]
32+
[DataRow("Action action1 = async () => { await Task.CompletedTask; }, action2 = () => { };")]
33+
[DataTestMethod]
34+
[Implemented]
35+
public void AssignAsyncVoidLambdaToAction_TestAnalyzer(string assertion)
36+
{
37+
var source = GenerateCode.AsyncFunctionStatement(assertion);
38+
39+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult
40+
{
41+
Id = AsyncVoidAnalyzer.DiagnosticId,
42+
Message = AsyncVoidAnalyzer.Message,
43+
Locations = new DiagnosticResultLocation[]
44+
{
45+
new DiagnosticResultLocation("Test0.cs", 10,13)
46+
},
47+
Severity = DiagnosticSeverity.Warning
48+
});
49+
}
50+
51+
[AssertionCodeFix(
52+
oldAssertion: "Action action = async () => { await Task.CompletedTask; };",
53+
newAssertion: "Func<Task> action = async () => { await Task.CompletedTask; };")]
54+
[AssertionCodeFix(
55+
oldAssertion: "Action action1 = async () => { await Task.CompletedTask; }, action2 = async () => { await Task.CompletedTask; };",
56+
newAssertion: "Func<Task> action1 = async () => { await Task.CompletedTask; }, action2 = async () => { await Task.CompletedTask; };")]
57+
[AssertionDataTestMethod]
58+
[NotImplemented]
59+
public void AssignAsyncVoidLambdaToAction_TestCodeFix(string oldAssertion, string newAssertion)
60+
{
61+
var oldSource = GenerateCode.AsyncFunctionStatement(oldAssertion);
62+
var newSource = GenerateCode.AsyncFunctionStatement(newAssertion);
63+
64+
DiagnosticVerifier.VerifyCSharpFix<AsyncVoidCodeFix, AsyncVoidAnalyzer>(oldSource, newSource);
65+
}
66+
}
67+
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ namespace FluentAssertions.Analyzers.Tests
66
[TestClass]
77
public class CollectionTests
88
{
9-
public TestContext TestContext { get; set; }
10-
119
[AssertionDataTestMethod]
1210
[AssertionDiagnostic("actual.Any().Should().BeTrue({0});")]
1311
[AssertionDiagnostic("actual.AsEnumerable().Any().Should().BeTrue({0}).And.ToString();")]

src/FluentAssertions.Analyzers/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public static class CodeSmell
9090
public const string Category = "FluentAssertionCodeSmell";
9191

9292
public const string NullConditionalAssertion = nameof(NullConditionalAssertion);
93+
public const string AsyncVoid = nameof(AsyncVoid);
9394
}
9495
}
9596
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CodeFixes;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
using System;
7+
using System.Collections.Immutable;
8+
using System.Composition;
9+
using System.Linq;
10+
using System.Threading.Tasks;
11+
12+
namespace FluentAssertions.Analyzers
13+
{
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public class AsyncVoidAnalyzer : DiagnosticAnalyzer
16+
{
17+
public const string DiagnosticId = Constants.CodeSmell.AsyncVoid;
18+
public const string Title = "Code Smell";
19+
public const string Message = "The assertions might not be executed when assigning an async void lambda to a Action";
20+
21+
public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, Message, Constants.CodeSmell.Category, DiagnosticSeverity.Warning, true);
22+
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
24+
25+
public sealed override void Initialize(AnalysisContext context)
26+
{
27+
context.EnableConcurrentExecution();
28+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
29+
context.RegisterCodeBlockAction(AnalyzeCodeBlock);
30+
}
31+
32+
private void AnalyzeCodeBlock(CodeBlockAnalysisContext context)
33+
{
34+
var method = context.CodeBlock as MethodDeclarationSyntax;
35+
if (method == null) return;
36+
37+
if (method.Body != null)
38+
{
39+
foreach (var statement in method.Body.Statements.OfType<LocalDeclarationStatementSyntax>())
40+
{
41+
42+
var diagnostic = AnalyzeStatement(context.SemanticModel, statement);
43+
if (diagnostic != null)
44+
{
45+
context.ReportDiagnostic(diagnostic);
46+
}
47+
}
48+
return;
49+
}
50+
}
51+
52+
protected virtual Diagnostic AnalyzeStatement(SemanticModel semanticModel, LocalDeclarationStatementSyntax statement)
53+
{
54+
var symbolInfo = semanticModel.GetSymbolInfo(statement.Declaration.Type);
55+
if (symbolInfo.Symbol?.Name != nameof(Action)) return null;
56+
57+
foreach (var variable in statement.Declaration.Variables)
58+
{
59+
if (variable.Initializer == null) continue;
60+
61+
if (!(variable.Initializer.Value is ParenthesizedLambdaExpressionSyntax lambda)) continue;
62+
63+
if (lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword))
64+
{
65+
return Diagnostic.Create(descriptor: Rule, location: statement.GetLocation());
66+
}
67+
}
68+
69+
return null;
70+
}
71+
}
72+
73+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AsyncVoidCodeFix)), Shared]
74+
public class AsyncVoidCodeFix : CodeFixProvider
75+
{
76+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AsyncVoidAnalyzer.DiagnosticId);
77+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
78+
{
79+
throw new NotImplementedException();
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)