Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support duck-typed awaitables and task-like types for Task/Async-related analyzers #1535

Merged
merged 18 commits into from
Sep 30, 2024
Merged
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
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))
Govorunb marked this conversation as resolved.
Show resolved Hide resolved
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))
Govorunb marked this conversation as resolved.
Show resolved Hide resolved
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