// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Microsoft.VisualStudio.Threading.Analyzers
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.VisualStudio.Threading;
///
/// Provides a code action to fix calls to synchronous methods from async methods when async options exist.
///
///
///
///
[ExportCodeFixProvider(LanguageNames.CSharp)]
public class VSTHRD103UseAsyncOptionCodeFix : CodeFixProvider
{
private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create(
VSTHRD103UseAsyncOptionAnalyzer.Id);
///
public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds;
///
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Diagnostic? diagnostic = context.Diagnostics.FirstOrDefault(d => d.Properties.ContainsKey(VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName));
if (diagnostic is object)
{
// Check that the method we're replacing the sync blocking call with actually exists.
// This is particularly useful when the method is an extension method, since the using directive
// would need to be present (or the namespace imply it) and we don't yet add missing using directives.
bool asyncAlternativeExists = false;
string asyncMethodName = diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName];
if (string.IsNullOrEmpty(asyncMethodName))
{
asyncMethodName = "GetAwaiter";
}
SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
SyntaxNode? syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var blockingIdentifier = syntaxRoot.FindNode(diagnostic.Location.SourceSpan) as IdentifierNameSyntax;
var memberAccessExpression = blockingIdentifier?.Parent as MemberAccessExpressionSyntax;
// Check whether this code was already calling the awaiter (in a synchronous fashion).
asyncAlternativeExists |= memberAccessExpression?.Expression is InvocationExpressionSyntax invoke && invoke.Expression is MemberAccessExpressionSyntax parentMemberAccess && parentMemberAccess.Name.Identifier.Text == nameof(Task.GetAwaiter);
if (!asyncAlternativeExists)
{
// If we fail to recognize the container, assume it exists since the analyzer thought it would.
ITypeSymbol? container = memberAccessExpression is object ? semanticModel.GetTypeInfo(memberAccessExpression.Expression, context.CancellationToken).ConvertedType : null;
asyncAlternativeExists = container is null || semanticModel.LookupSymbols(diagnostic.Location.SourceSpan.Start, name: asyncMethodName, container: container, includeReducedExtensionMethods: true).Any();
}
if (asyncAlternativeExists)
{
context.RegisterCodeFix(new ReplaceSyncMethodCallWithAwaitAsync(context.Document, diagnostic), diagnostic);
}
}
}
///
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
private class ReplaceSyncMethodCallWithAwaitAsync : CodeAction
{
private readonly Document document;
private readonly Diagnostic diagnostic;
internal ReplaceSyncMethodCallWithAwaitAsync(Document document, Diagnostic diagnostic)
{
this.document = document;
this.diagnostic = diagnostic;
}
public override string Title
{
get
{
return !string.IsNullOrEmpty(this.AlternativeAsyncMethod)
? string.Format(CultureInfo.CurrentCulture, Strings.AwaitXInstead, this.AlternativeAsyncMethod)
: Strings.UseAwaitInstead;
}
}
///
public override string? EquivalenceKey => null;
private string AlternativeAsyncMethod => this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName];
private string ExtensionMethodNamespace => this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.ExtensionMethodNamespaceKeyName];
protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken)
{
Document? document = this.document;
SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
// Find the synchronously blocking call member,
// and bookmark it so we can find it again after some mutations have taken place.
var syncAccessBookmark = new SyntaxAnnotation();
SimpleNameSyntax syncMethodName = (SimpleNameSyntax)root.FindNode(this.diagnostic.Location.SourceSpan);
if (syncMethodName is null)
{
MemberAccessExpressionSyntax? syncMemberAccess = root.FindNode(this.diagnostic.Location.SourceSpan).FirstAncestorOrSelf();
syncMethodName = syncMemberAccess.Name;
}
// When we give the Document a modified SyntaxRoot, yet another is created. So we first assign it to the Document,
// then we query for the SyntaxRoot from the Document.
document = document.WithSyntaxRoot(
root.ReplaceNode(syncMethodName, syncMethodName.WithAdditionalAnnotations(syncAccessBookmark)));
root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
syncMethodName = (SimpleNameSyntax)root.GetAnnotatedNodes(syncAccessBookmark).Single();
// We'll need the semantic model later. But because we've annotated a node, that changes the SyntaxRoot
// and that renders the default semantic model broken (even though we've already updated the document's SyntaxRoot?!).
// So after acquiring the semantic model, update it with the new method body.
SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
AnonymousFunctionExpressionSyntax? originalAnonymousMethodContainerIfApplicable = syncMethodName.FirstAncestorOrSelf();
MethodDeclarationSyntax? originalMethodDeclaration = syncMethodName.FirstAncestorOrSelf();
ISymbol? enclosingSymbol = semanticModel.GetEnclosingSymbol(this.diagnostic.Location.SourceSpan.Start, cancellationToken);
var hasReturnValue = ((enclosingSymbol as IMethodSymbol)?.ReturnType as INamedTypeSymbol)?.IsGenericType ?? false;
// Ensure that the method or anonymous delegate is using the async keyword.
MethodDeclarationSyntax updatedMethod;
if (originalAnonymousMethodContainerIfApplicable is object)
{
updatedMethod = originalMethodDeclaration.ReplaceNode(
originalAnonymousMethodContainerIfApplicable,
originalAnonymousMethodContainerIfApplicable.MakeMethodAsync(hasReturnValue, semanticModel, cancellationToken));
}
else
{
(document, updatedMethod) = await originalMethodDeclaration.MakeMethodAsync(document, cancellationToken).ConfigureAwait(false);
semanticModel = null; // out-dated
}
if (updatedMethod != originalMethodDeclaration)
{
// Re-discover our synchronously blocking member.
syncMethodName = (SimpleNameSyntax)updatedMethod.GetAnnotatedNodes(syncAccessBookmark).Single();
}
ExpressionSyntax? syncExpression = GetSynchronousExpression(syncMethodName);
ExpressionSyntax awaitExpression;
if (!string.IsNullOrEmpty(this.AlternativeAsyncMethod))
{
// Replace the member being called and await the invocation expression.
// While doing so, move leading trivia to the surrounding await expression.
SimpleNameSyntax? asyncMethodName = syncMethodName.WithIdentifier(SyntaxFactory.Identifier(this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName]));
awaitExpression = SyntaxFactory.AwaitExpression(
syncExpression.ReplaceNode(syncMethodName, asyncMethodName).WithoutLeadingTrivia())
.WithLeadingTrivia(syncExpression.GetLeadingTrivia());
}
else
{
// Remove the member being accessed that causes a synchronous block and simply await the object.
MemberAccessExpressionSyntax? syncMemberAccess = syncMethodName.FirstAncestorOrSelf();
ExpressionSyntax? syncMemberStrippedExpression = syncMemberAccess.Expression;
// Special case a common pattern of calling task.GetAwaiter().GetResult() and remove both method calls.
var expressionMethodCall = (syncMemberStrippedExpression as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax;
if (expressionMethodCall?.Name.Identifier.Text == nameof(Task.GetAwaiter))
{
syncMemberStrippedExpression = expressionMethodCall.Expression;
}
awaitExpression = SyntaxFactory.AwaitExpression(syncMemberStrippedExpression.WithoutLeadingTrivia())
.WithLeadingTrivia(syncMemberStrippedExpression.GetLeadingTrivia());
}
if (!(syncExpression.Parent is ExpressionStatementSyntax))
{
awaitExpression = SyntaxFactory.ParenthesizedExpression(awaitExpression)
.WithAdditionalAnnotations(Simplifier.Annotation);
}
updatedMethod = updatedMethod
.ReplaceNode(syncExpression, awaitExpression);
SyntaxNode? newRoot = root.ReplaceNode(originalMethodDeclaration, updatedMethod);
Document? newDocument = document.WithSyntaxRoot(newRoot);
return newDocument.Project.Solution;
}
private static ExpressionSyntax GetSynchronousExpression(SimpleNameSyntax syncMethodName)
{
SyntaxNode current = syncMethodName;
while (true)
{
switch (current.Kind())
{
case SyntaxKind.InvocationExpression:
return (ExpressionSyntax)current;
case SyntaxKind.SimpleMemberAccessExpression:
if (current.Parent.IsKind(SyntaxKind.InvocationExpression))
{
return (ExpressionSyntax)current.Parent;
}
else
{
return (ExpressionSyntax)current;
}
default:
current = current.Parent;
break;
}
}
}
}
}
}