diff --git a/ChangeLog.md b/ChangeLog.md index 856b5f0583..cbc405d806 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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 diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/DisposeResourceAsynchronouslyCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/DisposeResourceAsynchronouslyCodeFixProvider.cs index ae08c2cb9a..0d92721a74 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/DisposeResourceAsynchronouslyCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/DisposeResourceAsynchronouslyCodeFixProvider.cs @@ -120,7 +120,7 @@ private static async Task 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 @@ -138,7 +138,7 @@ private static async Task 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 @@ -156,7 +156,7 @@ private static async Task 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 @@ -174,7 +174,7 @@ private static async Task 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 diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs index 57eeda5920..801867c24c 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs @@ -66,7 +66,7 @@ private static async Task 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); @@ -78,7 +78,7 @@ private static async Task 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); @@ -92,7 +92,7 @@ private static async Task 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); @@ -106,7 +106,7 @@ private static async Task 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); @@ -120,7 +120,7 @@ private static async Task 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); diff --git a/src/Analyzers.CodeFixes/CSharp/SyntaxRewriters/UseAsyncAwaitRewriter.cs b/src/Analyzers.CodeFixes/CSharp/SyntaxRewriters/UseAsyncAwaitRewriter.cs index 53fb67f04c..0fddfcdeeb 100644 --- a/src/Analyzers.CodeFixes/CSharp/SyntaxRewriters/UseAsyncAwaitRewriter.cs +++ b/src/Analyzers.CodeFixes/CSharp/SyntaxRewriters/UseAsyncAwaitRewriter.cs @@ -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) diff --git a/src/Analyzers/CSharp/Analysis/ConfigureAwaitAnalyzer.cs b/src/Analyzers/CSharp/Analysis/ConfigureAwaitAnalyzer.cs index 60b3f2c0e9..7e0bbc50d2 100644 --- a/src/Analyzers/CSharp/Analysis/ConfigureAwaitAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/ConfigureAwaitAnalyzer.cs @@ -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; @@ -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"); @@ -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) @@ -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() + .Any(method => method.ReturnType.IsAwaitable(semanticModel, position) + && method.HasSingleParameter(SpecialType.System_Boolean)); + } } diff --git a/src/Analyzers/CSharp/Analysis/DisposeResourceAsynchronouslyAnalyzer.cs b/src/Analyzers/CSharp/Analysis/DisposeResourceAsynchronouslyAnalyzer.cs index c16ef55ab7..042e13762a 100644 --- a/src/Analyzers/CSharp/Analysis/DisposeResourceAsynchronouslyAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/DisposeResourceAsynchronouslyAnalyzer.cs @@ -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); } diff --git a/src/Analyzers/CSharp/Analysis/NonAsynchronousMethodNameShouldNotEndWithAsyncAnalyzer.cs b/src/Analyzers/CSharp/Analysis/NonAsynchronousMethodNameShouldNotEndWithAsyncAnalyzer.cs index 5d28c61e1f..17379f68c9 100644 --- a/src/Analyzers/CSharp/Analysis/NonAsynchronousMethodNameShouldNotEndWithAsyncAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/NonAsynchronousMethodNameShouldNotEndWithAsyncAnalyzer.cs @@ -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 => { @@ -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; @@ -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; @@ -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; diff --git a/src/Analyzers/CSharp/Analysis/RemoveRedundantAsyncAwaitAnalyzer.cs b/src/Analyzers/CSharp/Analysis/RemoveRedundantAsyncAwaitAnalyzer.cs index cb01fc95c4..bd02a6e1bb 100644 --- a/src/Analyzers/CSharp/Analysis/RemoveRedundantAsyncAwaitAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/RemoveRedundantAsyncAwaitAnalyzer.cs @@ -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; diff --git a/src/Analyzers/CSharp/Analysis/UseAsyncAwaitAnalyzer.cs b/src/Analyzers/CSharp/Analysis/UseAsyncAwaitAnalyzer.cs index 2222534541..68d16c9fb5 100644 --- a/src/Analyzers/CSharp/Analysis/UseAsyncAwaitAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/UseAsyncAwaitAnalyzer.cs @@ -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)) @@ -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)) @@ -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)) @@ -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)) @@ -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)) diff --git a/src/Common/CSharp/Analysis/RemoveAsyncAwaitAnalysis.cs b/src/Common/CSharp/Analysis/RemoveAsyncAwaitAnalysis.cs index 49300e7312..84abb81616 100644 --- a/src/Common/CSharp/Analysis/RemoveAsyncAwaitAnalysis.cs +++ b/src/Common/CSharp/Analysis/RemoveAsyncAwaitAnalysis.cs @@ -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); @@ -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); @@ -417,7 +417,7 @@ 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); @@ -425,7 +425,7 @@ private static bool VerifyAwaitType(AwaitExpressionSyntax awaitExpression, IType 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( diff --git a/src/Core/MetadataNames.cs b/src/Core/MetadataNames.cs index df61ea6ce1..a19d62a4cd 100644 --- a/src/Core/MetadataNames.cs +++ b/src/Core/MetadataNames.cs @@ -51,9 +51,11 @@ internal static class MetadataNames public static readonly MetadataName System_ReadOnlySpan_T = MetadataName.Parse("System.ReadOnlySpan`1"); public static readonly MetadataName System_Reflection = MetadataName.Parse("System.Reflection"); public static readonly MetadataName System_Runtime_CompilerServices = MetadataName.Parse("System.Runtime.CompilerServices"); + public static readonly MetadataName System_Runtime_CompilerServices_AsyncMethodBuilderAttribute = MetadataName.Parse("System.Runtime.CompilerServices.AsyncMethodBuilderAttribute"); public static readonly MetadataName System_Runtime_CompilerServices_CollectionBuilderAttribute = MetadataName.Parse("System.Runtime.CompilerServices.CollectionBuilderAttribute"); public static readonly MetadataName System_Runtime_CompilerServices_ConfiguredTaskAwaitable = MetadataName.Parse("System.Runtime.CompilerServices.ConfiguredTaskAwaitable"); public static readonly MetadataName System_Runtime_CompilerServices_ConfiguredTaskAwaitable_T = MetadataName.Parse("System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1"); + public static readonly MetadataName System_Runtime_CompilerServices_INotifyCompletion = MetadataName.Parse("System.Runtime.CompilerServices.INotifyCompletion"); public static readonly MetadataName System_Runtime_InteropServices_LayoutKind = MetadataName.Parse("System.Runtime.InteropServices.LayoutKind"); public static readonly MetadataName System_Runtime_InteropServices_StructLayoutAttribute = MetadataName.Parse("System.Runtime.InteropServices.StructLayoutAttribute"); public static readonly MetadataName System_Runtime_Serialization_DataMemberAttribute = MetadataName.Parse("System.Runtime.Serialization.DataMemberAttribute"); diff --git a/src/Core/SymbolUtility.cs b/src/Core/SymbolUtility.cs index 06b3130a5c..5da3690e29 100644 --- a/src/Core/SymbolUtility.cs +++ b/src/Core/SymbolUtility.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Immutable; using System.Linq; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; namespace Roslynator; @@ -623,53 +624,73 @@ public static ulong GetEnumValueAsUInt64(object value, INamedTypeSymbol enumType } } - public static bool IsAwaitable(ITypeSymbol typeSymbol, bool shouldCheckWindowsRuntimeTypes = false) + public static bool IsWellKnownTaskType(this ITypeSymbol typeSymbol) { - INamedTypeSymbol? namedTypeSymbol = GetPossiblyAwaitableType(typeSymbol); + return typeSymbol.ContainingNamespace.HasMetadataName(in MetadataNames.System_Threading_Tasks) + && (typeSymbol.MetadataName is "Task" or "Task`1" or "ValueTask" or "ValueTask`1"); + } + + /// + /// Determines if the symbol is an awaitable type (i.e. the keyword can be used on it) or a method that returns one.
+ /// A type is awaitable if it has an instance or extension GetAwaiter method that returns a correctly-shaped awaiter type. + ///
+ /// + /// For more information, see the C# language specification. + /// + /// Used to determine whether a candidate GetAwaiter method is accessible at the given . + /// The position of the token at which the is used. + /// A indicating whether the symbol is an awaitable type or a method that returns one. + public static bool IsAwaitable(this ISymbol? symbol, SemanticModel semanticModel, int position) + { + ITypeSymbol? typeSymbol = (symbol as ITypeSymbol) ?? (symbol as IMethodSymbol)?.ReturnType; - if (namedTypeSymbol is null) + if (typeSymbol is null or { SpecialType: SpecialType.System_Void }) return false; - INamedTypeSymbol originalDefinition = namedTypeSymbol.OriginalDefinition; - TypeKind typeKind = originalDefinition.TypeKind; + if (typeSymbol.IsWellKnownTaskType()) + return true; - if (typeKind == TypeKind.Struct - && originalDefinition.ContainingNamespace.HasMetadataName(MetadataNames.System_Threading_Tasks)) - { - switch (originalDefinition.MetadataName) - { - case "ValueTask": - case "ValueTask`1": - return true; - } - } + return HasAwaitableShape(typeSymbol, semanticModel, position); + } - if (typeKind == TypeKind.Class - && namedTypeSymbol.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task)) - { - return true; - } + private static bool HasAwaitableShape(ITypeSymbol typeSymbol, SemanticModel semanticModel, int position) + { + // this is the same check as in Roslyn, reimplemented due to it being internal + // https://github.com/dotnet/roslyn/blob/a182892bf997a457cfcdbece5352e1a139eb2a12/src/Compilers/CSharp/Portable/Binder/Binder_Await.cs#L281-L289 - if (shouldCheckWindowsRuntimeTypes) - { - if (typeKind == TypeKind.Interface - && originalDefinition.ContainingNamespace.HasMetadataName(MetadataNames.WinRT.Windows_Foundation)) - { - switch (originalDefinition.MetadataName) - { - case "IAsyncAction": - case "IAsyncActionWithProgress`1": - case "IAsyncOperation`1": - case "IAsyncOperationWithProgress`2": - return true; - } - } + IMethodSymbol? getAwaiter = semanticModel.LookupSymbols(position, typeSymbol, WellKnownMemberNames.GetAwaiter, includeReducedExtensionMethods: true) + .OfType() + .FirstOrDefault(); - if (namedTypeSymbol.Implements(MetadataNames.WinRT.Windows_Foundation_IAsyncAction, allInterfaces: true)) - return true; - } + if (getAwaiter is not { Parameters.Length: 0 }) + return false; - return false; + var awaiterTypeDefinition = getAwaiter.ReturnType.OriginalDefinition as INamedTypeSymbol; + if (awaiterTypeDefinition is not { SpecialType: SpecialType.None }) + return false; + + // bool IsCompleted { get; } + IPropertySymbol? isCompletedProp = semanticModel.LookupSymbols(position, awaiterTypeDefinition, WellKnownMemberNames.IsCompleted) + .OfType() + .FirstOrDefault(); + + if (isCompletedProp is not { Type.SpecialType: SpecialType.System_Boolean, GetMethod: not null }) + return false; + + if (!semanticModel.IsAccessible(position, isCompletedProp.GetMethod)) + return false; + + // implements INotifyCompletion + if (!awaiterTypeDefinition.Implements(MetadataNames.System_Runtime_CompilerServices_INotifyCompletion, allInterfaces: true)) + return false; + + // void GetResult() || T GetResult() + // must be an instance method + IMethodSymbol? getResultMethod = semanticModel.LookupSymbols(position, awaiterTypeDefinition, WellKnownMemberNames.GetResult) + .OfType() + .FirstOrDefault(); + + return getResultMethod is { Parameters.Length: 0, TypeParameters.Length: 0 }; } internal static INamedTypeSymbol? GetPossiblyAwaitableType(ITypeSymbol typeSymbol) @@ -695,4 +716,36 @@ public static bool IsAwaitable(ITypeSymbol typeSymbol, bool shouldCheckWindowsRu return typeSymbol as INamedTypeSymbol; } + + /// + /// Determines if a type: + /// + /// + /// Is a task type - that is, , , , , or a user-implemented task-like type.
+ /// Only task types (and ) can be the return type of an method. + ///
+ /// Can be awaited with the keyword (see ). + ///
+ ///
+ /// + /// For more information on task types, see the C# language specification. + /// + public static bool IsAwaitableTaskType(this ITypeSymbol typeSymbol, SemanticModel semanticModel, int position) + { + if (typeSymbol.OriginalDefinition is not INamedTypeSymbol definition) + return false; + + if (definition.IsWellKnownTaskType()) + return true; + + // Roslyn checks arity, see https://github.com/dotnet/roslyn/blob/c4203b867e9c0287cabb0a0674bfca096c08fd3e/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs#L1844 + if (definition.Arity > 1) + return false; + + // Task (and Task) are hardcoded and don't have an AsyncMethodBuilder attribute. + // see https://github.com/dotnet/roslyn/blob/c4203b867e9c0287cabb0a0674bfca096c08fd3e/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs#L1778-L1806 + bool isTaskLike = definition.HasAttribute(in MetadataNames.System_Runtime_CompilerServices_AsyncMethodBuilderAttribute); + + return isTaskLike && HasAwaitableShape(definition, semanticModel, position); + } } diff --git a/src/Tests/Analyzers.Tests/RCS1046AsynchronousMethodNameShouldEndWithAsyncTests.cs b/src/Tests/Analyzers.Tests/RCS1046AsynchronousMethodNameShouldEndWithAsyncTests.cs index e7e83b35d0..d9d2ba5a72 100644 --- a/src/Tests/Analyzers.Tests/RCS1046AsynchronousMethodNameShouldEndWithAsyncTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1046AsynchronousMethodNameShouldEndWithAsyncTests.cs @@ -100,6 +100,42 @@ class C "); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AsynchronousMethodNameShouldEndWithAsync)] + public async Task Test_DuckTyped() + { + await VerifyDiagnosticAsync(@" +class C +{ + DuckTyped [|Foo|]() => new(); + DuckTyped [|Foo2|]() => new(); +} + +class DuckTyped +{ + public CustomAwaiter GetAwaiter() => new(); +} + +class DuckTyped +{ + public CustomAwaiter GetAwaiter() => new(); +} + +struct CustomAwaiter : System.Runtime.CompilerServices.INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } +} + +struct CustomAwaiter : System.Runtime.CompilerServices.INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(System.Action continuation) { } + public T GetResult() => default(T); +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AsynchronousMethodNameShouldEndWithAsync)] public async Task TestNoDiagnostic_EntryPointMethod() { diff --git a/src/Tests/Analyzers.Tests/RCS1047NonAsynchronousMethodNameShouldNotEndWithAsyncTests.cs b/src/Tests/Analyzers.Tests/RCS1047NonAsynchronousMethodNameShouldNotEndWithAsyncTests.cs index b7cd971a78..3691dab2f4 100644 --- a/src/Tests/Analyzers.Tests/RCS1047NonAsynchronousMethodNameShouldNotEndWithAsyncTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1047NonAsynchronousMethodNameShouldNotEndWithAsyncTests.cs @@ -133,6 +133,46 @@ ValueTask ValueTaskAsync() { return default(ValueTask); } + DuckTyped DuckTypedAsync() + { + return default(DuckTyped); + } + DuckTyped DuckTypedGenericAsync() + { + return default(DuckTyped); + } + T DuckTypedAsync() where T : DuckTyped + { + return default(T); + } + T DuckTypedGenericAsync() where T : DuckTyped + { + return default(T); + } +} + +class DuckTyped +{ + public CustomAwaiter GetAwaiter() => new(); +} + +class DuckTyped +{ + public CustomAwaiter GetAwaiter() => new(); +} + +struct CustomAwaiter : System.Runtime.CompilerServices.INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } +} + +struct CustomAwaiter : System.Runtime.CompilerServices.INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(System.Action continuation) { } + public T GetResult() => default(T); } "); } diff --git a/src/Tests/Analyzers.Tests/RCS1090AddCallToConfigureAwaitTests.cs b/src/Tests/Analyzers.Tests/RCS1090AddCallToConfigureAwaitTests.cs index a8835fb2b2..24eed0b59c 100644 --- a/src/Tests/Analyzers.Tests/RCS1090AddCallToConfigureAwaitTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1090AddCallToConfigureAwaitTests.cs @@ -245,6 +245,168 @@ async Task M() "); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] + public async Task Test_DuckTyped() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await [|M2()|]; + } + DuckTyped M2() => default(DuckTyped); +} + +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + + public ConfiguredDuckTyped ConfigureAwait(bool continueOnCapturedContext) + { + return default(ConfiguredDuckTyped); + } + public struct ConfiguredDuckTyped + { + public ConfiguredDuckAwaiter GetAwaiter() => default(ConfiguredDuckAwaiter); + + public struct ConfiguredDuckAwaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + } +} +", @" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await M2().ConfigureAwait(false); + } + DuckTyped M2() => default(DuckTyped); +} + +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + + public ConfiguredDuckTyped ConfigureAwait(bool continueOnCapturedContext) + { + return default(ConfiguredDuckTyped); + } + public struct ConfiguredDuckTyped + { + public ConfiguredDuckAwaiter GetAwaiter() => default(ConfiguredDuckAwaiter); + + public struct ConfiguredDuckAwaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] + public async Task Test_ExtensionMethod() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await [|Task.Yield()|]; + } +} + +static class E +{ + public static ConfiguredYieldAwaitable ConfigureAwait(this YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + return default(ConfiguredYieldAwaitable); + } +} + +struct ConfiguredYieldAwaitable +{ + public ConfiguredYieldAwaitable(YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + } + + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } +} +", @" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await Task.Yield().ConfigureAwait(false); + } +} + +static class E +{ + public static ConfiguredYieldAwaitable ConfigureAwait(this YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + return default(ConfiguredYieldAwaitable); + } +} + +struct ConfiguredYieldAwaitable +{ + public ConfiguredYieldAwaitable(YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + } + + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] public async Task Test_Indentation() { @@ -343,6 +505,87 @@ async Task M() ValueTask M2() => default; } +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] + public async Task TestNoDiagnostic_ExtensionMethod() + { + await VerifyNoDiagnosticAsync(@" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await Task.Yield().ConfigureAwait(false); + } +} + +static class E +{ + public static ConfiguredYieldAwaitable ConfigureAwait(this YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + return default(ConfiguredYieldAwaitable); + } +} + +struct ConfiguredYieldAwaitable +{ + public ConfiguredYieldAwaitable(YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + } + + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] + public async Task TestNoDiagnostic_Awaitable_Lookalike() + { + await VerifyNoDiagnosticAsync(@" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await M2(); + } + Awaitable M2() => default; +} + +struct Awaitable +{ + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + + public NonAwaitable ConfigureAwait(bool continueOnCapturedContext) + { + return default(NonAwaitable); + } +} + +struct NonAwaitable +{ + // no awaiter +} "); } } diff --git a/src/Tests/Analyzers.Tests/RCS1090RemoveCallToConfigureAwaitTests.cs b/src/Tests/Analyzers.Tests/RCS1090RemoveCallToConfigureAwaitTests.cs index f238609013..1ad0d7240b 100644 --- a/src/Tests/Analyzers.Tests/RCS1090RemoveCallToConfigureAwaitTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1090RemoveCallToConfigureAwaitTests.cs @@ -245,6 +245,168 @@ async Task M() "); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] + public async Task Test_DuckTyped() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await M2()[|.ConfigureAwait(false)|]; + } + DuckTyped M2() => default(DuckTyped); +} + +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + + public ConfiguredDuckTyped ConfigureAwait(bool continueOnCapturedContext) + { + return default(ConfiguredDuckTyped); + } + public struct ConfiguredDuckTyped + { + public ConfiguredDuckAwaiter GetAwaiter() => default(ConfiguredDuckAwaiter); + + public struct ConfiguredDuckAwaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + } +} +", @" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await M2(); + } + DuckTyped M2() => default(DuckTyped); +} + +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + + public ConfiguredDuckTyped ConfigureAwait(bool continueOnCapturedContext) + { + return default(ConfiguredDuckTyped); + } + public struct ConfiguredDuckTyped + { + public ConfiguredDuckAwaiter GetAwaiter() => default(ConfiguredDuckAwaiter); + + public struct ConfiguredDuckAwaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] + public async Task Test_ExtensionMethod() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await Task.Yield()[|.ConfigureAwait(false)|]; + } +} + +static class E +{ + public static ConfiguredYieldAwaitable ConfigureAwait(this YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + return default(ConfiguredYieldAwaitable); + } +} + +struct ConfiguredYieldAwaitable +{ + public ConfiguredYieldAwaitable(YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + } + + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } +} +", @" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await Task.Yield(); + } +} + +static class E +{ + public static ConfiguredYieldAwaitable ConfigureAwait(this YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + return default(ConfiguredYieldAwaitable); + } +} + +struct ConfiguredYieldAwaitable +{ + public ConfiguredYieldAwaitable(YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + } + + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] public async Task TestNoDiagnostic_Task() { @@ -310,6 +472,87 @@ async Task M() ValueTask M2() => default; } +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] + public async Task TestNoDiagnostic_ExtensionMethod() + { + await VerifyNoDiagnosticAsync(@" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await Task.Yield(); + } +} + +static class E +{ + public static ConfiguredYieldAwaitable ConfigureAwait(this YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + return default(ConfiguredYieldAwaitable); + } +} + +struct ConfiguredYieldAwaitable +{ + public ConfiguredYieldAwaitable(YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + } + + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ConfigureAwait)] + public async Task TestNoDiagnostic_NonAwaitable_Lookalike() + { + await VerifyNoDiagnosticAsync(@" +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async Task M() + { + await M2().ConfigureAwait(false); + } + NonAwaitable M2() => default; +} + +struct Awaitable +{ + public Awaiter GetAwaiter() => default(Awaiter); + + public struct Awaiter : INotifyCompletion + { + public bool IsCompleted => false; + public void OnCompleted(System.Action continuation) { } + public void GetResult() { } + } +} + +struct NonAwaitable +{ + // no awaiter + + public Awaitable ConfigureAwait(bool continueOnCapturedContext) + { + return default(Awaitable); + } +} "); } } diff --git a/src/Tests/Analyzers.Tests/RCS1174RemoveRedundantAsyncAwaitTests.cs b/src/Tests/Analyzers.Tests/RCS1174RemoveRedundantAsyncAwaitTests.cs index 9c8c1e8d21..e7b11bc8da 100644 --- a/src/Tests/Analyzers.Tests/RCS1174RemoveRedundantAsyncAwaitTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1174RemoveRedundantAsyncAwaitTests.cs @@ -533,6 +533,78 @@ Task GetAsync() """); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveRedundantAsyncAwait)] + public async Task Test_DuckTyped_TaskType() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + [|async|] DuckTyped M2() + { + return await M2(); + } + + [|async|] DuckTyped MC2() + { + return await MC2().ConfigureAwait(false); + } +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +static class ConfigureAwaitExtensions +{ + public static DuckTyped ConfigureAwait(this DuckTyped instance, bool __) => instance; +} +", @" +using System; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + DuckTyped M2() + { + return M2(); + } + + DuckTyped MC2() + { + return MC2(); + } +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +static class ConfigureAwaitExtensions +{ + public static DuckTyped ConfigureAwait(this DuckTyped instance, bool __) => instance; +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveRedundantAsyncAwait)] public async Task TestNoDiagnostic_IfElse_ReturnWithoutAwait() { diff --git a/src/Tests/Analyzers.Tests/RCS1229UseAsyncAwaitTests.cs b/src/Tests/Analyzers.Tests/RCS1229UseAsyncAwaitTests.cs index 2403b7ffe9..b8a917d74e 100644 --- a/src/Tests/Analyzers.Tests/RCS1229UseAsyncAwaitTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1229UseAsyncAwaitTests.cs @@ -452,7 +452,111 @@ async Task M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnusedElementInDocumentationComment)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] + public async Task Test_DuckTyped_TaskType() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + DuckTyped [|M|]() + { + using (default(IDisposable)) + { + return GetAsync(); + } + } + + DuckTyped [|M2|]() + { + using (default(IDisposable)) + { + return GetAsync(); + } + } + + DuckTyped GetAsync() => default; + DuckTyped GetAsync() => default; +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public void GetResult() { } +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +", @" +using System; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + async DuckTyped M() + { + using (default(IDisposable)) + { + await GetAsync(); + } + } + + async DuckTyped M2() + { + using (default(IDisposable)) + { + return await GetAsync(); + } + } + + DuckTyped GetAsync() => default; + DuckTyped GetAsync() => default; +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public void GetResult() { } +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] public async Task TestNoDiagnostic_UsingLocalDeclaration() { await VerifyNoDiagnosticAsync(@" @@ -477,7 +581,7 @@ public Task M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnusedElementInDocumentationComment)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] public async Task TestNoDiagnostic_TaskCompletedTask() { await VerifyNoDiagnosticAsync(@" @@ -496,7 +600,7 @@ Task M(CancellationToken cancellationToken) "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnusedElementInDocumentationComment)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] public async Task TestNoDiagnostic_TaskFromCanceled() { await VerifyNoDiagnosticAsync(@" @@ -515,7 +619,7 @@ Task M(CancellationToken cancellationToken) "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnusedElementInDocumentationComment)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] public async Task TestNoDiagnostic_TaskFromException() { await VerifyNoDiagnosticAsync(@" @@ -534,7 +638,7 @@ Task M(CancellationToken cancellationToken) "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnusedElementInDocumentationComment)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] public async Task TestNoDiagnostic_TaskOfTFromResult() { await VerifyNoDiagnosticAsync(@" @@ -553,7 +657,7 @@ Task M(CancellationToken cancellationToken) "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnusedElementInDocumentationComment)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] public async Task TestNoDiagnostic_TaskOfTFromCanceled() { await VerifyNoDiagnosticAsync(@" @@ -572,7 +676,7 @@ Task M(CancellationToken cancellationToken) "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnusedElementInDocumentationComment)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] public async Task TestNoDiagnostic_TaskOfTFromException() { await VerifyNoDiagnosticAsync(@" @@ -591,7 +695,7 @@ Task M(CancellationToken cancellationToken) "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnusedElementInDocumentationComment)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] public async Task TestNoDiagnostic_IAsyncEnumerable() { await VerifyNoDiagnosticAsync(@" @@ -615,6 +719,96 @@ async IAsyncEnumerable GetAsync2(IDisposable disposable) yield break; } } +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] + public async Task TestNoDiagnostic_DuckTyped_NotTaskType() + { + await VerifyNoDiagnosticAsync(@" +using System; +using System.Runtime.CompilerServices; + +class C +{ + DuckTyped M() + { + using (default(IDisposable)) + { + return GetAsync(); + } + } + + DuckTyped M2() + { + using (default(IDisposable)) + { + return GetAsync(); + } + } + + DuckTyped GetAsync() => default; + DuckTyped GetAsync() => default; +} + +//[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +//[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} + +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public void GetResult() { } +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseAsyncAwait)] + public async Task TestNoDiagnostic_NonAwaitable_TaskType() + { + await VerifyNoDiagnosticAsync(@" +using System; +using System.Runtime.CompilerServices; + +class C +{ + NonAwaitableTaskType M() + { + using (default(IDisposable)) + { + return GetAsync(); + } + } + + NonAwaitableTaskType M2() + { + using (default(IDisposable)) + { + return GetAsync(); + } + } + + NonAwaitableTaskType GetAsync() => default; + NonAwaitableTaskType GetAsync() => default; +} + +[AsyncMethodBuilder(null)] +class NonAwaitableTaskType { } +[AsyncMethodBuilder(null)] +class NonAwaitableTaskType { } "); } } diff --git a/src/Tests/Analyzers.Tests/RCS1261DisposeResourceAsynchronouslyTests.cs b/src/Tests/Analyzers.Tests/RCS1261DisposeResourceAsynchronouslyTests.cs index cf910750ac..36ba8ff8f5 100644 --- a/src/Tests/Analyzers.Tests/RCS1261DisposeResourceAsynchronouslyTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1261DisposeResourceAsynchronouslyTests.cs @@ -922,6 +922,202 @@ internal class Disposable : IDisposable, IAsyncDisposable "); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_DuckTyped_TaskType_WithAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.IO; +using System.Runtime.CompilerServices; + +class C +{ + async DuckTyped M() + { + [|using|] FileStream fs = default; + await GetAsync(); + } + + async DuckTyped M2() + { + [|using|] FileStream fs = default; + return await GetAsync(); + } + + DuckTyped GetAsync() => default; + DuckTyped GetAsync() => default; +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} + +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public void GetResult() { } +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +", @" +using System; +using System.IO; +using System.Runtime.CompilerServices; + +class C +{ + async DuckTyped M() + { + await using FileStream fs = default; + await GetAsync(); + } + + async DuckTyped M2() + { + await using FileStream fs = default; + return await GetAsync(); + } + + DuckTyped GetAsync() => default; + DuckTyped GetAsync() => default; +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} + +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public void GetResult() { } +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_DuckTyped_TaskType_WithoutAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.IO; +using System.Runtime.CompilerServices; + +class C +{ + DuckTyped M() + { + [|using|] FileStream fs = default; + return GetAsync(); + } + + DuckTyped M2() + { + [|using|] FileStream fs = default; + return GetAsync(); + } + + DuckTyped GetAsync() => default; + DuckTyped GetAsync() => default; +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} + +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public void GetResult() { } +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +", @" +using System; +using System.IO; +using System.Runtime.CompilerServices; + +class C +{ + async DuckTyped M() + { + await using FileStream fs = default; + await GetAsync(); + } + + async DuckTyped M2() + { + await using FileStream fs = default; + return await GetAsync(); + } + + DuckTyped GetAsync() => default; + DuckTyped GetAsync() => default; +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} + +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public void GetResult() { } +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] public async Task TestNoDiagnostic_LockStatement() { @@ -945,4 +1141,89 @@ async Task Foo() } """); } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task TestNoDiagnostic_DuckTyped_NotTaskType() + { + await VerifyNoDiagnosticAsync(@" +using System; +using System.IO; +using System.Runtime.CompilerServices; + +class C +{ + DuckTyped M() + { + using FileStream fs = default; + return GetAsync(); + } + + DuckTyped M2() + { + using FileStream fs = default; + return GetAsync(); + } + + DuckTyped GetAsync() => default; + DuckTyped GetAsync() => default; +} + +//[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +//[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} + +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public void GetResult() { } +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task TestNoDiagnostic_NonAwaitable_TaskType() + { + await VerifyNoDiagnosticAsync(@" +using System; +using System.IO; +using System.Runtime.CompilerServices; + +class C +{ + NonAwaitableTaskType M() + { + using FileStream fs = default; + return GetAsync(); + } + + NonAwaitableTaskType M2() + { + using FileStream fs = default; + return GetAsync(); + } + + NonAwaitableTaskType GetAsync() => default; + NonAwaitableTaskType GetAsync() => default; +} + +[AsyncMethodBuilder(null)] +class NonAwaitableTaskType { } +[AsyncMethodBuilder(null)] +class NonAwaitableTaskType { } +"); + } } diff --git a/src/Tests/Refactorings.Tests/RR0209RemoveAsyncAwaitTests.cs b/src/Tests/Refactorings.Tests/RR0209RemoveAsyncAwaitTests.cs index 9eeda70fa0..dd661738a6 100644 --- a/src/Tests/Refactorings.Tests/RR0209RemoveAsyncAwaitTests.cs +++ b/src/Tests/Refactorings.Tests/RR0209RemoveAsyncAwaitTests.cs @@ -525,4 +525,66 @@ Task GetAsync() } """, equivalenceKey: EquivalenceKey.Create(RefactoringId)); } + + [Fact, Trait(Traits.Refactoring, RefactoringIdentifiers.RemoveAsyncAwait)] + public async Task Test_DuckTyped_TaskType() + { + await VerifyRefactoringAsync(@" +using System; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + [||]async DuckTyped M() + { + return await M().ConfigureAwait(false); + } +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +static class ConfigureAwaitExtensions +{ + public static DuckTyped ConfigureAwait(this DuckTyped instance, bool __) => instance; +} +", @" +using System; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +class C +{ + DuckTyped M() + { + return M(); + } +} + +[AsyncMethodBuilder(null)] +class DuckTyped +{ + public Awaiter GetAwaiter() => default(Awaiter); +} +public struct Awaiter : INotifyCompletion +{ + public bool IsCompleted => true; + public void OnCompleted(Action continuation) { } + public T GetResult() => default(T); +} +static class ConfigureAwaitExtensions +{ + public static DuckTyped ConfigureAwait(this DuckTyped instance, bool __) => instance; +} +", equivalenceKey: EquivalenceKey.Create(RefactoringId)); + } } diff --git a/src/Workspaces.Common/CSharp/Refactorings/RemoveAsyncAwait.cs b/src/Workspaces.Common/CSharp/Refactorings/RemoveAsyncAwait.cs index ca77bf502f..f69e2177c7 100644 --- a/src/Workspaces.Common/CSharp/Refactorings/RemoveAsyncAwait.cs +++ b/src/Workspaces.Common/CSharp/Refactorings/RemoveAsyncAwait.cs @@ -109,19 +109,14 @@ private static ExpressionSyntax ExtractExpressionFromAwait(AwaitExpressionSyntax { ExpressionSyntax expression = awaitExpression.Expression; - if (semanticModel.GetTypeSymbol(expression, cancellationToken) is INamedTypeSymbol typeSymbol) + if (semanticModel.GetTypeSymbol(expression, cancellationToken) is INamedTypeSymbol typeSymbol + && typeSymbol.IsAwaitable(semanticModel, expression.SpanStart) + && expression is InvocationExpressionSyntax invocation) { - if (typeSymbol.HasMetadataName(MetadataNames.System_Runtime_CompilerServices_ConfiguredTaskAwaitable) - || typeSymbol.OriginalDefinition.HasMetadataName(MetadataNames.System_Runtime_CompilerServices_ConfiguredTaskAwaitable_T)) - { - if (expression is InvocationExpressionSyntax invocation) - { - var memberAccess = invocation.Expression as MemberAccessExpressionSyntax; + var memberAccess = invocation.Expression as MemberAccessExpressionSyntax; - if (string.Equals(memberAccess?.Name?.Identifier.ValueText, "ConfigureAwait", StringComparison.Ordinal)) - expression = memberAccess.Expression; - } - } + if (string.Equals(memberAccess?.Name?.Identifier.ValueText, "ConfigureAwait", StringComparison.Ordinal)) + expression = memberAccess.Expression; } return expression.WithTriviaFrom(awaitExpression);