Skip to content

Commit

Permalink
Support duck-typed awaitables and task-like types for Task/Async-rela…
Browse files Browse the repository at this point in the history
…ted analyzers (#1535)
  • Loading branch information
Govorunb authored Sep 30, 2024
1 parent fca5b4e commit 7be911c
Show file tree
Hide file tree
Showing 21 changed files with 1,362 additions and 121 deletions.
11 changes: 11 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix analyzer [RCS1140](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1140) ([PR](https://github.com/dotnet/roslynator/pull/1524))
- Fix analyzer [RCS1077](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1077) ([PR](https://github.com/dotnet/roslynator/pull/1544))

### Changed
- Add support for duck-typed awaitables and task-like types for Task/Async-related analyzers ([PR](https://github.com/dotnet/roslynator/pull/1535))
- Affects the following analyzers:
- [RCS1046](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1046)
- [RCS1047](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1047)
- [RCS1090](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1090)
- [RCS1174](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1174)
- [RCS1229](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1229)
- [RCS1261](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1261)
- Affects refactoring [RR0209](https://josefpihrt.github.io/docs/roslynator/refactorings/RR0209)

## [4.12.6] - 2024-09-23

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private static async Task<Document> RefactorAsync(

IMethodSymbol methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
var newBody = (BlockSyntax)rewriter.VisitBlock(newNode.Body);

newNode = newNode
Expand All @@ -138,7 +138,7 @@ private static async Task<Document> RefactorAsync(

IMethodSymbol methodSymbol = semanticModel.GetDeclaredSymbol(localFunction, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
var newBody = (BlockSyntax)rewriter.VisitBlock(newNode.Body);

newNode = newNode
Expand All @@ -156,7 +156,7 @@ private static async Task<Document> RefactorAsync(

var methodSymbol = (IMethodSymbol)semanticModel.GetSymbol(lambdaExpression, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)newNode.Body);

newNode = newNode
Expand All @@ -174,7 +174,7 @@ private static async Task<Document> RefactorAsync(

var methodSymbol = (IMethodSymbol)semanticModel.GetSymbol(anonymousMethod, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)newNode.Body);

newNode = newNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private static async Task<Document> RefactorAsync(
{
IMethodSymbol methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);

var newNode = (MethodDeclarationSyntax)rewriter.VisitMethodDeclaration(methodDeclaration);

Expand All @@ -78,7 +78,7 @@ private static async Task<Document> RefactorAsync(
{
IMethodSymbol methodSymbol = semanticModel.GetDeclaredSymbol(localFunction, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);

var newBody = (BlockSyntax)rewriter.VisitBlock(localFunction.Body);

Expand All @@ -92,7 +92,7 @@ private static async Task<Document> RefactorAsync(
{
var methodSymbol = (IMethodSymbol)semanticModel.GetSymbol(lambda, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);

var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)lambda.Body);

Expand All @@ -106,7 +106,7 @@ private static async Task<Document> RefactorAsync(
{
var methodSymbol = (IMethodSymbol)semanticModel.GetSymbol(lambda, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);

var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)lambda.Body);

Expand All @@ -120,7 +120,7 @@ private static async Task<Document> RefactorAsync(
{
var methodSymbol = (IMethodSymbol)semanticModel.GetSymbol(anonymousMethod, cancellationToken);

UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);

var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)anonymousMethod.Body);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,14 @@ private UseAsyncAwaitRewriter(bool keepReturnStatement)

public bool KeepReturnStatement { get; }

public static UseAsyncAwaitRewriter Create(IMethodSymbol methodSymbol)
public static UseAsyncAwaitRewriter Create(IMethodSymbol methodSymbol, SemanticModel semanticModel, int position)
{
ITypeSymbol returnType = methodSymbol.ReturnType.OriginalDefinition;

var keepReturnStatement = false;
bool keepReturnStatement = returnType is INamedTypeSymbol { Arity: 1 }
&& returnType.IsAwaitable(semanticModel, position);

if (returnType.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_ValueTask_T)
|| returnType.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task_T))
{
keepReturnStatement = true;
}

return new UseAsyncAwaitRewriter(keepReturnStatement: keepReturnStatement);
return new UseAsyncAwaitRewriter(keepReturnStatement);
}

public override SyntaxNode VisitReturnStatement(ReturnStatementSyntax node)
Expand Down
66 changes: 41 additions & 25 deletions src/Analyzers/CSharp/Analysis/ConfigureAwaitAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -65,7 +66,10 @@ private static void AddCallToConfigureAwait(SyntaxNodeAnalysisContext context)
if (typeSymbol is null)
return;

if (!SymbolUtility.IsAwaitable(typeSymbol))
if (!typeSymbol.IsAwaitable(context.SemanticModel, expression.SpanStart))
return;

if (!IsConfigureAwaitable(typeSymbol, context.SemanticModel, expression.SpanStart))
return;

DiagnosticHelpers.ReportDiagnostic(context, DiagnosticRules.ConfigureAwait, awaitExpression.Expression, "Add");
Expand All @@ -75,39 +79,43 @@ private static void RemoveCallToConfigureAwait(SyntaxNodeAnalysisContext context
{
var awaitExpression = (AwaitExpressionSyntax)context.Node;

// await (expr).ConfigureAwait(false);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ExpressionSyntax expression = awaitExpression.Expression;

// await (expr).ConfigureAwait(false);
// ^^^^^^^^^^^^^^^^^^^^^^
SimpleMemberInvocationExpressionInfo invocationInfo = SyntaxInfo.SimpleMemberInvocationExpressionInfo(expression);

if (!IsConfigureAwait(expression))
if (!IsConfigureAwait(invocationInfo))
return;

ITypeSymbol typeSymbol = context.SemanticModel.GetTypeSymbol(expression, context.CancellationToken);
ITypeSymbol awaitedType = context.SemanticModel.GetTypeSymbol(expression, context.CancellationToken);

if (typeSymbol is null)
if (awaitedType is null)
return;

switch (typeSymbol.MetadataName)
{
case "ConfiguredTaskAwaitable":
case "ConfiguredTaskAwaitable`1":
case "ConfiguredValueTaskAwaitable":
case "ConfiguredValueTaskAwaitable`1":
{
if (typeSymbol.ContainingNamespace.HasMetadataName(MetadataNames.System_Runtime_CompilerServices))
{
DiagnosticHelpers.ReportDiagnostic(
context,
DiagnosticRules.ConfigureAwait,
Location.Create(
awaitExpression.SyntaxTree,
TextSpan.FromBounds(invocationInfo.OperatorToken.SpanStart, expression.Span.End)),
"Remove");
}

break;
}
}
if (!awaitedType.IsAwaitable(context.SemanticModel, expression.SpanStart))
return;

// await (expr).ConfigureAwait(false);
// ^^^^
// This expression may not be awaitable, in which case removing ConfigureAwait is not possible.
ITypeSymbol configuredType = context.SemanticModel.GetTypeSymbol(invocationInfo.Expression, context.CancellationToken);

if (configuredType is null)
return;

if (!configuredType.IsAwaitable(context.SemanticModel, invocationInfo.Expression.SpanStart))
return;

DiagnosticHelpers.ReportDiagnostic(
context,
DiagnosticRules.ConfigureAwait,
Location.Create(
awaitExpression.SyntaxTree,
TextSpan.FromBounds(invocationInfo.OperatorToken.SpanStart, expression.Span.End)),
"Remove");
}

public static bool IsConfigureAwait(ExpressionSyntax expression)
Expand All @@ -124,4 +132,12 @@ private static bool IsConfigureAwait(SimpleMemberInvocationExpressionInfo invoca
&& string.Equals(invocationInfo.NameText, "ConfigureAwait")
&& invocationInfo.Arguments.Count == 1;
}

private static bool IsConfigureAwaitable(ITypeSymbol typeSymbol, SemanticModel semanticModel, int position)
{
return semanticModel.LookupSymbols(position, typeSymbol, "ConfigureAwait", includeReducedExtensionMethods: true)
.OfType<IMethodSymbol>()
.Any(method => method.ReturnType.IsAwaitable(semanticModel, position)
&& method.HasSingleParameter(SpecialType.System_Boolean));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ private static void Analyze(
: context.SemanticModel.GetSymbol(containingMethod, context.CancellationToken)) as IMethodSymbol;

if (methodSymbol?.IsErrorType() == false
&& SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
&& methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, context.Node.SpanStart))
{
ReportDiagnostic(context, usingKeyword);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ public override void Initialize(AnalysisContext context)

context.RegisterCompilationStartAction(startContext =>
{
INamedTypeSymbol asyncAction = startContext.Compilation.GetTypeByMetadataName("Windows.Foundation.IAsyncAction");

bool shouldCheckWindowsRuntimeTypes = asyncAction is not null;

startContext.RegisterSyntaxNodeAction(
c =>
{
Expand All @@ -50,14 +46,14 @@ public override void Initialize(AnalysisContext context)
DiagnosticRules.AsynchronousMethodNameShouldEndWithAsync,
DiagnosticRules.NonAsynchronousMethodNameShouldNotEndWithAsync))
{
AnalyzeMethodDeclaration(c, shouldCheckWindowsRuntimeTypes);
AnalyzeMethodDeclaration(c);
}
},
SyntaxKind.MethodDeclaration);
});
}

private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context, bool shouldCheckWindowsRuntimeTypes)
private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;

Expand All @@ -74,7 +70,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context,
if (!methodSymbol.Name.EndsWith("Async", StringComparison.Ordinal))
return;

if (SymbolUtility.IsAwaitable(methodSymbol.ReturnType, shouldCheckWindowsRuntimeTypes)
if (methodSymbol.ReturnType.IsAwaitable(context.SemanticModel, methodDeclaration.SpanStart)
|| IsAsyncEnumerableLike(methodSymbol.ReturnType.OriginalDefinition))
{
return;
Expand Down Expand Up @@ -105,7 +101,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context,
if (methodSymbol.ImplementsInterfaceMember(allInterfaces: true))
return;

if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType, shouldCheckWindowsRuntimeTypes)
if (!methodSymbol.ReturnType.IsAwaitable(context.SemanticModel, methodDeclaration.SpanStart)
&& !methodSymbol.ReturnType.OriginalDefinition.HasMetadataName(in MetadataNames.System_Collections_Generic_IAsyncEnumerable_T))
{
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ void ReportAwaitAndConfigureAwait(AwaitExpressionSyntax awaitExpression)

ITypeSymbol typeSymbol = context.SemanticModel.GetTypeSymbol(expression, context.CancellationToken);

if (typeSymbol?.OriginalDefinition.HasMetadataName(MetadataNames.System_Runtime_CompilerServices_ConfiguredTaskAwaitable_T) == true
if (typeSymbol?.OriginalDefinition.IsAwaitable(context.SemanticModel, expression.SpanStart) == true
&& (expression is InvocationExpressionSyntax invocation))
{
var memberAccess = invocation.Expression as MemberAccessExpressionSyntax;
Expand Down
16 changes: 9 additions & 7 deletions src/Analyzers/CSharp/Analysis/UseAsyncAwaitAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
if (!body.Statements.Any())
return;

IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);
if (context.SemanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken) is not IMethodSymbol methodSymbol)
return;

if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
return;

if (IsFixable(body, context))
Expand All @@ -79,9 +80,10 @@ private static void AnalyzeLocalFunctionStatement(SyntaxNodeAnalysisContext cont
if (!body.Statements.Any())
return;

IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(localFunction, context.CancellationToken);
if (context.SemanticModel.GetDeclaredSymbol(localFunction, context.CancellationToken) is not IMethodSymbol methodSymbol)
return;

if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
return;

if (IsFixable(body, context))
Expand All @@ -101,7 +103,7 @@ private static void AnalyzeSimpleLambdaExpression(SyntaxNodeAnalysisContext cont
if (context.SemanticModel.GetSymbol(simpleLambda, context.CancellationToken) is not IMethodSymbol methodSymbol)
return;

if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
return;

if (IsFixable(body, context))
Expand All @@ -121,7 +123,7 @@ private static void AnalyzeParenthesizedLambdaExpression(SyntaxNodeAnalysisConte
if (context.SemanticModel.GetSymbol(parenthesizedLambda, context.CancellationToken) is not IMethodSymbol methodSymbol)
return;

if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
return;

if (IsFixable(body, context))
Expand All @@ -143,7 +145,7 @@ private static void AnalyzeAnonymousMethodExpression(SyntaxNodeAnalysisContext c
if (context.SemanticModel.GetSymbol(anonymousMethod, context.CancellationToken) is not IMethodSymbol methodSymbol)
return;

if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
return;

if (IsFixable(body, context))
Expand Down
8 changes: 4 additions & 4 deletions src/Common/CSharp/Analysis/RemoveAsyncAwaitAnalysis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ private static bool VerifyTypes(

ITypeSymbol returnType = methodSymbol.ReturnType;

if (returnType?.OriginalDefinition.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task_T) != true)
if (returnType?.OriginalDefinition.IsAwaitable(semanticModel, node.SpanStart) != true)
return false;

ITypeSymbol typeArgument = ((INamedTypeSymbol)returnType).TypeArguments.SingleOrDefault(shouldThrow: false);
Expand Down Expand Up @@ -394,7 +394,7 @@ private static bool VerifyTypes(

ITypeSymbol returnType = methodSymbol.ReturnType;

if (returnType?.OriginalDefinition.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task_T) != true)
if (returnType?.OriginalDefinition.IsAwaitable(semanticModel, node.SpanStart) != true)
return false;

ITypeSymbol typeArgument = ((INamedTypeSymbol)returnType).TypeArguments.SingleOrDefault(shouldThrow: false);
Expand All @@ -417,15 +417,15 @@ private static bool VerifyAwaitType(AwaitExpressionSyntax awaitExpression, IType
if (expressionTypeSymbol is null)
return false;

if (expressionTypeSymbol.OriginalDefinition.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task_T))
if (expressionTypeSymbol.OriginalDefinition.IsAwaitable(semanticModel, expression.SpanStart))
return true;

SimpleMemberInvocationExpressionInfo invocationInfo = SyntaxInfo.SimpleMemberInvocationExpressionInfo(expression);

return invocationInfo.Success
&& invocationInfo.Arguments.Count == 1
&& invocationInfo.NameText == "ConfigureAwait"
&& expressionTypeSymbol.OriginalDefinition.HasMetadataName(MetadataNames.System_Runtime_CompilerServices_ConfiguredTaskAwaitable_T);
&& expressionTypeSymbol.OriginalDefinition.IsAwaitable(semanticModel, expression.SpanStart);
}

private static IMethodSymbol GetMethodSymbol(
Expand Down
Loading

0 comments on commit 7be911c

Please sign in to comment.