Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions docfx/analyzers/VSTHRD107.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# VSTHRD107 Await Task within using expression

The C# `using` statement requires that the used expression implement `IDisposable`.
The C# `using` statement and `using` declaration require that the used expression or variable implement `IDisposable`.
Because `Task<T>` implements `IDisposable`, one may accidentally omit an `await` operator
and `Dispose` of the `Task<T>` instead of the `T` result itself when `T` derives from `IDisposable`.

Expand All @@ -12,16 +12,30 @@ using (lck.EnterAsync())
{
// ...
}

using (var releaser = lck.EnterAsync())
{
// ...
}

using var releaser = lck.EnterAsync();
```

## Solution

Add the `await` operator within the `using` expression.
Add the `await` operator within the `using` expression or variable initializer.

```csharp
AsyncSemaphore lck;
using (await lck.EnterAsync())
{
// ...
}

using (var releaser = await lck.EnterAsync())
{
// ...
}

using var releaser = await lck.EnterAsync();
```
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
Expand Down Expand Up @@ -47,29 +47,73 @@ public class VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer : DiagnosticAnalyze
/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Descriptor);

/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterSyntaxNodeAction(
Utils.DebuggableWrapper(this.AnalyzeNode),
Utils.DebuggableWrapper(this.AnalyzeUsingStatement),
SyntaxKind.UsingStatement);
context.RegisterSyntaxNodeAction(
Utils.DebuggableWrapper(this.AnalyzeLocalDeclaration),
SyntaxKind.LocalDeclarationStatement);
}

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
private void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context)
{
var usingStatement = (UsingStatementSyntax)context.Node;
if (usingStatement.Expression is object)

// In C# 8+, an asynchronous using statement (`await using (...)`) disposes the target
// asynchronously via IAsyncDisposable. This analyzer only flags unawaited tasks in synchronous
// using statements that dispose via IDisposable.
if (usingStatement.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"if the await keyword is an await keyword"?? This deserves a code comment explaining why it makes sense.

{
return;
}

if (usingStatement.Expression is not null)
{
this.CheckAndReport(context, usingStatement.Expression);
}
else if (usingStatement.Declaration is not null)
{
this.CheckAndReport(context, usingStatement.Declaration);
}
}

private void AnalyzeLocalDeclaration(SyntaxNodeAnalysisContext context)
{
var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

// Check synchronous `using var x = ...;` declarations.
// Skip `await using var x = ...;` because it targets IAsyncDisposable rather than IDisposable.
if (localDeclaration.UsingKeyword.IsKind(SyntaxKind.UsingKeyword) &&
!localDeclaration.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))
{
this.CheckAndReport(context, localDeclaration.Declaration);
}
}

private void CheckAndReport(SyntaxNodeAnalysisContext context, VariableDeclarationSyntax variableDeclaration)
{
foreach (VariableDeclaratorSyntax variable in variableDeclaration.Variables)
{
TypeInfo expressionTypeInfo = context.SemanticModel.GetTypeInfo(usingStatement.Expression, context.CancellationToken);
ITypeSymbol? expressionType = expressionTypeInfo.Type;
if (expressionType?.Name == nameof(Task) &&
expressionType.BelongsToNamespace(Namespaces.SystemThreadingTasks))
if (variable.Initializer?.Value is { } initValue)
{
context.ReportDiagnostic(
Diagnostic.Create(Descriptor, usingStatement.Expression.GetLocation()));
this.CheckAndReport(context, initValue);
}
}
}

private void CheckAndReport(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
{
TypeInfo expressionTypeInfo = context.SemanticModel.GetTypeInfo(expression, context.CancellationToken);
if (Utils.IsTask(expressionTypeInfo.Type))
{
context.ReportDiagnostic(
Diagnostic.Create(Descriptor, expression.GetLocation()));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
Expand Down Expand Up @@ -56,25 +56,32 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
Document? document = context.Document;
SyntaxNode? root = await document.GetSyntaxRootOrThrowAsync(ct).ConfigureAwait(false);
MethodDeclarationSyntax method = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<MethodDeclarationSyntax>() ?? throw new InvalidOperationException("Unable to find MethodDeclaration.");
MethodDeclarationSyntax? method = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<MethodDeclarationSyntax>();
Comment thread
austinaviv marked this conversation as resolved.

(document, method, _) = await FixUtils.UpdateDocumentAsync(
document,
method,
m =>
{
root = m.SyntaxTree.GetRoot(ct);
UsingStatementSyntax? usingStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<UsingStatementSyntax>() ?? throw new InvalidOperationException("Unable to find using statement.");
AwaitExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression(
SyntaxFactory.ParenthesizedExpression(usingStatement.Expression!));
UsingStatementSyntax modifiedUsingStatement = usingStatement.WithExpression(awaitExpression)
.WithAdditionalAnnotations(Simplifier.Annotation);
return m.ReplaceNode(usingStatement, modifiedUsingStatement);
},
ct).ConfigureAwait(false);
(document, method) = await method.MakeMethodAsync(document, ct).ConfigureAwait(false);
if (method is not null)
{
(document, method, _) = await FixUtils.UpdateDocumentAsync(
document,
method,
m =>
{
root = m.SyntaxTree.GetRoot(ct);
ExpressionSyntax expression = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true).FirstAncestorOrSelf<ExpressionSyntax>()
?? throw new InvalidOperationException("Unable to find expression.");
return (MethodDeclarationSyntax)ReplaceExpressionWithAwait(m, expression);
},
ct).ConfigureAwait(false);
(document, method) = await method.MakeMethodAsync(document, ct).ConfigureAwait(false);

return document.Project.Solution;
return document.Project.Solution;
}
else
{
ExpressionSyntax expression = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true).FirstAncestorOrSelf<ExpressionSyntax>()
?? throw new InvalidOperationException("Unable to find expression.");
SyntaxNode newRoot = ReplaceExpressionWithAwait(root, expression);
return document.WithSyntaxRoot(newRoot).Project.Solution;
}
},
"only action"),
diagnostic);
Expand All @@ -84,4 +91,30 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)

/// <inheritdoc />
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

private static SyntaxNode ReplaceExpressionWithAwait(SyntaxNode syntaxRoot, ExpressionSyntax expression)
{
// We're moving trivia from the original expression to the overall await expression.
ExpressionSyntax expressionToAwait = expression.WithoutTrivia();

// Complex expressions should be parenthesized to ensure the `await` is applied to the whole expression.
if (expressionToAwait is not (InvocationExpressionSyntax or IdentifierNameSyntax or MemberAccessExpressionSyntax or ElementAccessExpressionSyntax))
{
expressionToAwait = SyntaxFactory.ParenthesizedExpression(expressionToAwait);
}

AwaitExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression(expressionToAwait)
.WithTriviaFrom(expression)
.WithAdditionalAnnotations(Simplifier.Annotation);

StatementSyntax? statement = expression.FirstAncestorOrSelf<StatementSyntax>();
if (statement is not null)
{
StatementSyntax newStatement = statement.ReplaceNode(expression, awaitExpression)
Comment thread
austinaviv marked this conversation as resolved.
.WithAdditionalAnnotations(Simplifier.Annotation);
return syntaxRoot.ReplaceNode(statement, newStatement);
}

return syntaxRoot.ReplaceNode(expression, awaitExpression);
}
}
Loading