Skip to content

Commit fb54e8b

Browse files
authored
UnnecessaryAsync analyzer becomes smarter with dataflow analysis (#9)
1 parent aaea043 commit fb54e8b

File tree

3 files changed

+203
-18
lines changed

3 files changed

+203
-18
lines changed

AsyncFixer.Test/AsyncCallInsideUsingBlockTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ static async void foo()
183183
using (var stream = new FileStream(""existing"", FileMode.Open))
184184
{
185185
await stream.CopyToAsync(newStream);
186-
stream.CopyToAsync(Stream.Null);
186+
newStream.CopyToAsync(Stream.Null);
187187
}
188188
}
189189
}";

AsyncFixer.Test/UnnecessaryAsyncTests.cs

+114
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,120 @@ class Program
575575
VerifyCSharpDiagnostic(test);
576576
}
577577

578+
[Fact]
579+
public void NoWarn_UsingStatement()
580+
{
581+
var test = @"
582+
using System;
583+
using System.Threading.Tasks;
584+
585+
class Program
586+
{
587+
async Task foo()
588+
{
589+
using var stream = FileStream.Null;
590+
var stream2 = FileStream.Null;
591+
await Task.Run(() => stream2.CopyToAsync(stream));
592+
}
593+
}";
594+
595+
VerifyCSharpDiagnostic(test);
596+
}
597+
598+
[Fact]
599+
public void NoWarn_UsingStatement2()
600+
{
601+
var test = @"
602+
using System;
603+
using System.Threading.Tasks;
604+
605+
class Program
606+
{
607+
async Task foo()
608+
{
609+
using var stream = FileStream.Null;
610+
await stream.ReadAsync(null);
611+
}
612+
}";
613+
614+
VerifyCSharpDiagnostic(test);
615+
}
616+
617+
[Fact]
618+
public void NoWarn_UsingStatementWithDataflow()
619+
{
620+
var test = @"
621+
using System;
622+
using System.Threading.Tasks;
623+
624+
class Program
625+
{
626+
async Task foo()
627+
{
628+
using var stream = new MemoryStream();
629+
int streamOperation()
630+
{
631+
return stream.Read(null);
632+
}
633+
634+
await Task.Run(() => streamOperation());
635+
}
636+
}";
637+
638+
VerifyCSharpDiagnostic(test);
639+
}
640+
641+
[Fact]
642+
public void NoWarn_UsingStatementWithDataflow2()
643+
{
644+
var test = @"
645+
using System;
646+
using System.Threading.Tasks;
647+
648+
class Program
649+
{
650+
async Task foo()
651+
{
652+
using var stream = new MemoryStream();
653+
int streamOperation()
654+
{
655+
return stream.Read(null);
656+
}
657+
658+
var t = Task.Run(() => streamOperation());
659+
await t;
660+
}
661+
}";
662+
663+
VerifyCSharpDiagnostic(test);
664+
}
665+
666+
[Fact(Skip = "TODO: fix the dataflow analysis to analyze all locations of the symbol, not only the definitions")]
667+
public void NoWarn_UsingStatementWithDataflowComplicated()
668+
{
669+
var test = @"
670+
using System;
671+
using System.Threading.Tasks;
672+
673+
class Program
674+
{
675+
async Task foo()
676+
{
677+
using var stream = new MemoryStream();
678+
int streamOperation()
679+
{
680+
return stream.Read(null);
681+
}
682+
683+
Task t = null;
684+
t = Task.Run(() => streamOperation());
685+
await t;
686+
}
687+
}";
688+
689+
VerifyCSharpDiagnostic(test);
690+
}
691+
578692
protected override CodeFixProvider GetCSharpCodeFixProvider()
579693
{
580694
return new UnnecessaryAsyncFixer();

AsyncFixer/UnnecessaryAsync/UnnecessaryAsyncAnalyzer.cs

+88-17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using Microsoft.CodeAnalysis.CSharp.Syntax;
77
using Microsoft.CodeAnalysis.Diagnostics;
88
using Microsoft.CodeAnalysis.Operations;
9+
using System.Collections.Generic;
10+
using System;
911

1012
namespace AsyncFixer.UnnecessaryAsync
1113
{
@@ -63,7 +65,7 @@ private void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
6365
// Retrieve all await expressions excluding the ones under lambda functions.
6466
var awaitExpressions = node.DescendantNodes().OfType<AwaitExpressionSyntax>().Where(a => a.FirstAncestorOrSelfUnderGivenNode<LambdaExpressionSyntax>(node) == null).ToList();
6567

66-
if (node.Body == null &&
68+
if (node.Body == null &&
6769
node.ExpressionBody?.Expression.Kind() == SyntaxKind.AwaitExpression)
6870
{
6971
// Expression-bodied members
@@ -96,6 +98,13 @@ private void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
9698
var controlFlow = context.SemanticModel.AnalyzeControlFlow(node.Body);
9799
var returnStatements = controlFlow?.ReturnStatements ?? ImmutableArray<SyntaxNode>.Empty;
98100

101+
// Retrieve the disposable object identifiers from the using statements.
102+
// For instance, for the following statement, we'd like to return 'source'.
103+
// using FileStream source = File.Open("data", FileMode.Open);
104+
var disposableObjectNames = node.Body.DescendantNodes().OfType<LocalDeclarationStatementSyntax>()
105+
.Where(a => a.UsingKeyword.Kind() != SyntaxKind.None)
106+
.SelectMany(a => a.DescendantNodes().OfType<VariableDeclaratorSyntax>().Select(b => b.Identifier.ValueText)).ToList();
107+
99108
var numAwait = 0;
100109
if (returnStatements.Any())
101110
{
@@ -107,7 +116,7 @@ private void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
107116
return;
108117
}
109118

110-
if (HasUsingOrTryParent(returnStatement, node))
119+
if (!IsSafeToRemoveAsyncAwait(returnStatement))
111120
{
112121
return;
113122
}
@@ -125,13 +134,18 @@ private void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
125134
else
126135
{
127136
// if awaitExpression is the last statement's expression
137+
// using (var stream = new MemoryStream())
138+
// {
139+
// await Task.FromResult(3); ==> this is NOT the last statement because of the using block.
140+
// }
141+
128142
var lastStatement = node.Body.Statements.LastOrDefault();
129143
if ((lastStatement as ExpressionStatementSyntax)?.Expression?.Kind() != SyntaxKind.AwaitExpression)
130144
{
131145
return;
132146
}
133147

134-
if (HasUsingOrTryParent(lastStatement, node))
148+
if (!IsSafeToRemoveAsyncAwait(lastStatement))
135149
{
136150
return;
137151
}
@@ -144,26 +158,83 @@ private void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
144158
return;
145159
}
146160

147-
// Make sure that we do not give a warning about the await statement involving a disposable object.
161+
context.ReportDiagnostic(diagnostic);
148162

149-
// Retrieve the disposable object identifiers from the using statements.
150-
// For instance, for the following statement, we'd like to return 'source'.
151-
// using FileStream source = File.Open("data", FileMode.Open);
152-
var disposableObjectNames = node.Body.DescendantNodes().OfType<LocalDeclarationStatementSyntax>()
153-
.Where(a => a.UsingKeyword.Kind() != SyntaxKind.None)
154-
.SelectMany(a => a.DescendantNodes().OfType<VariableDeclaratorSyntax>().Select(b => b.Identifier.ValueText));
155-
if (disposableObjectNames.Any())
163+
bool IsSafeToRemoveAsyncAwait(StatementSyntax statement)
156164
{
157-
// There are disposable objects.
158-
// Let's check whether at least one await expression uses one of those disposable objects.
159-
if (awaitExpressions.SelectMany(a => a.DescendantNodes().OfType<IdentifierNameSyntax>())
160-
.Any(a => disposableObjectNames.Contains(a.Identifier.ValueText)))
165+
if (HasUsingOrTryParent(statement, node))
161166
{
162-
return;
167+
// If it is under 'using' or 'try' block, it is not safe to remove async/await.
168+
return false;
169+
}
170+
171+
// Make sure that we do not give a warning about the await statement involving an object which is created by an using statement.
172+
// We use dataflow analysis to accurately detect a case like below:
173+
// async Task foo()
174+
// {
175+
// using var stream = new MemoryStream();
176+
// int streamOperation()
177+
// {
178+
// return stream.Read(null);
179+
// }
180+
//
181+
// var t = Task.Run(() => streamOperation())
182+
// await t;
183+
// }
184+
// In the example above, we need to find out whether 'stream' is accessed in the last statement.
185+
186+
var names = GetAccessedVariableNamesWithPointsToAnalysis(context.SemanticModel, node, statement).ToList();
187+
188+
if (names.Any(a => disposableObjectNames.Contains(a)))
189+
{
190+
return false;
163191
}
192+
193+
return true;
164194
}
195+
}
165196

166-
context.ReportDiagnostic(diagnostic);
197+
/// <summary>
198+
/// Return the names of all variables that are read-accessed in the given statement.
199+
/// </summary>
200+
/// <remarks>
201+
/// The method iteratively goes through the definitions to find implicitly-accessed variables.
202+
/// </remarks>
203+
private IEnumerable<string> GetAccessedVariableNamesWithPointsToAnalysis(SemanticModel semanticModel, SyntaxNode root, SyntaxNode node, int depth = 0)
204+
{
205+
if (depth == 5 || node == null || root == null)
206+
{
207+
// Put a limit for the call stack frame
208+
yield break;
209+
}
210+
211+
var dataFlowResult = semanticModel.AnalyzeDataFlow(node);
212+
if (dataFlowResult?.Succeeded == true)
213+
{
214+
foreach (ISymbol symbol in dataFlowResult.ReadInside)
215+
{
216+
yield return symbol.Name;
217+
218+
if (symbol.DeclaringSyntaxReferences == null)
219+
{
220+
continue;
221+
}
222+
223+
foreach (var syntaxRef in symbol.DeclaringSyntaxReferences)
224+
{
225+
var expressions = root.FindNode(syntaxRef.Span, getInnermostNodeForTie: true).DescendantNodes((n) => !(n is ExpressionSyntax)).OfType<ExpressionSyntax>();
226+
227+
foreach (var expr in expressions)
228+
{
229+
var names = GetAccessedVariableNamesWithPointsToAnalysis(semanticModel, root, expr, depth + 1);
230+
foreach (var name in names)
231+
{
232+
yield return name;
233+
}
234+
}
235+
}
236+
}
237+
}
167238
}
168239
}
169240
}

0 commit comments

Comments
 (0)