Skip to content

Commit 7be911c

Browse files
authored
Support duck-typed awaitables and task-like types for Task/Async-related analyzers (#1535)
1 parent fca5b4e commit 7be911c

21 files changed

+1362
-121
lines changed

ChangeLog.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Fix analyzer [RCS1140](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1140) ([PR](https://github.com/dotnet/roslynator/pull/1524))
1515
- Fix analyzer [RCS1077](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1077) ([PR](https://github.com/dotnet/roslynator/pull/1544))
1616

17+
### Changed
18+
- Add support for duck-typed awaitables and task-like types for Task/Async-related analyzers ([PR](https://github.com/dotnet/roslynator/pull/1535))
19+
- Affects the following analyzers:
20+
- [RCS1046](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1046)
21+
- [RCS1047](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1047)
22+
- [RCS1090](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1090)
23+
- [RCS1174](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1174)
24+
- [RCS1229](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1229)
25+
- [RCS1261](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1261)
26+
- Affects refactoring [RR0209](https://josefpihrt.github.io/docs/roslynator/refactorings/RR0209)
27+
1728
## [4.12.6] - 2024-09-23
1829

1930
### Added

src/Analyzers.CodeFixes/CSharp/CodeFixes/DisposeResourceAsynchronouslyCodeFixProvider.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ private static async Task<Document> RefactorAsync(
120120

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

123-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
123+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
124124
var newBody = (BlockSyntax)rewriter.VisitBlock(newNode.Body);
125125

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

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

141-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
141+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
142142
var newBody = (BlockSyntax)rewriter.VisitBlock(newNode.Body);
143143

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

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

159-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
159+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
160160
var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)newNode.Body);
161161

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

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

177-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
177+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
178178
var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)newNode.Body);
179179

180180
newNode = newNode

src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private static async Task<Document> RefactorAsync(
6666
{
6767
IMethodSymbol methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken);
6868

69-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
69+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
7070

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

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

81-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
81+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
8282

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

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

95-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
95+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
9696

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

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

109-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
109+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
110110

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

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

123-
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol);
123+
UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol, semanticModel, node.SpanStart);
124124

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

src/Analyzers.CodeFixes/CSharp/SyntaxRewriters/UseAsyncAwaitRewriter.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,14 @@ private UseAsyncAwaitRewriter(bool keepReturnStatement)
2424

2525
public bool KeepReturnStatement { get; }
2626

27-
public static UseAsyncAwaitRewriter Create(IMethodSymbol methodSymbol)
27+
public static UseAsyncAwaitRewriter Create(IMethodSymbol methodSymbol, SemanticModel semanticModel, int position)
2828
{
2929
ITypeSymbol returnType = methodSymbol.ReturnType.OriginalDefinition;
3030

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

33-
if (returnType.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_ValueTask_T)
34-
|| returnType.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task_T))
35-
{
36-
keepReturnStatement = true;
37-
}
38-
39-
return new UseAsyncAwaitRewriter(keepReturnStatement: keepReturnStatement);
34+
return new UseAsyncAwaitRewriter(keepReturnStatement);
4035
}
4136

4237
public override SyntaxNode VisitReturnStatement(ReturnStatementSyntax node)

src/Analyzers/CSharp/Analysis/ConfigureAwaitAnalyzer.cs

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
22

33
using System.Collections.Immutable;
4+
using System.Linq;
45
using Microsoft.CodeAnalysis;
56
using Microsoft.CodeAnalysis.CSharp;
67
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -65,7 +66,10 @@ private static void AddCallToConfigureAwait(SyntaxNodeAnalysisContext context)
6566
if (typeSymbol is null)
6667
return;
6768

68-
if (!SymbolUtility.IsAwaitable(typeSymbol))
69+
if (!typeSymbol.IsAwaitable(context.SemanticModel, expression.SpanStart))
70+
return;
71+
72+
if (!IsConfigureAwaitable(typeSymbol, context.SemanticModel, expression.SpanStart))
6973
return;
7074

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

82+
// await (expr).ConfigureAwait(false);
83+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7884
ExpressionSyntax expression = awaitExpression.Expression;
7985

86+
// await (expr).ConfigureAwait(false);
87+
// ^^^^^^^^^^^^^^^^^^^^^^
8088
SimpleMemberInvocationExpressionInfo invocationInfo = SyntaxInfo.SimpleMemberInvocationExpressionInfo(expression);
8189

82-
if (!IsConfigureAwait(expression))
90+
if (!IsConfigureAwait(invocationInfo))
8391
return;
8492

85-
ITypeSymbol typeSymbol = context.SemanticModel.GetTypeSymbol(expression, context.CancellationToken);
93+
ITypeSymbol awaitedType = context.SemanticModel.GetTypeSymbol(expression, context.CancellationToken);
8694

87-
if (typeSymbol is null)
95+
if (awaitedType is null)
8896
return;
8997

90-
switch (typeSymbol.MetadataName)
91-
{
92-
case "ConfiguredTaskAwaitable":
93-
case "ConfiguredTaskAwaitable`1":
94-
case "ConfiguredValueTaskAwaitable":
95-
case "ConfiguredValueTaskAwaitable`1":
96-
{
97-
if (typeSymbol.ContainingNamespace.HasMetadataName(MetadataNames.System_Runtime_CompilerServices))
98-
{
99-
DiagnosticHelpers.ReportDiagnostic(
100-
context,
101-
DiagnosticRules.ConfigureAwait,
102-
Location.Create(
103-
awaitExpression.SyntaxTree,
104-
TextSpan.FromBounds(invocationInfo.OperatorToken.SpanStart, expression.Span.End)),
105-
"Remove");
106-
}
107-
108-
break;
109-
}
110-
}
98+
if (!awaitedType.IsAwaitable(context.SemanticModel, expression.SpanStart))
99+
return;
100+
101+
// await (expr).ConfigureAwait(false);
102+
// ^^^^
103+
// This expression may not be awaitable, in which case removing ConfigureAwait is not possible.
104+
ITypeSymbol configuredType = context.SemanticModel.GetTypeSymbol(invocationInfo.Expression, context.CancellationToken);
105+
106+
if (configuredType is null)
107+
return;
108+
109+
if (!configuredType.IsAwaitable(context.SemanticModel, invocationInfo.Expression.SpanStart))
110+
return;
111+
112+
DiagnosticHelpers.ReportDiagnostic(
113+
context,
114+
DiagnosticRules.ConfigureAwait,
115+
Location.Create(
116+
awaitExpression.SyntaxTree,
117+
TextSpan.FromBounds(invocationInfo.OperatorToken.SpanStart, expression.Span.End)),
118+
"Remove");
111119
}
112120

113121
public static bool IsConfigureAwait(ExpressionSyntax expression)
@@ -124,4 +132,12 @@ private static bool IsConfigureAwait(SimpleMemberInvocationExpressionInfo invoca
124132
&& string.Equals(invocationInfo.NameText, "ConfigureAwait")
125133
&& invocationInfo.Arguments.Count == 1;
126134
}
135+
136+
private static bool IsConfigureAwaitable(ITypeSymbol typeSymbol, SemanticModel semanticModel, int position)
137+
{
138+
return semanticModel.LookupSymbols(position, typeSymbol, "ConfigureAwait", includeReducedExtensionMethods: true)
139+
.OfType<IMethodSymbol>()
140+
.Any(method => method.ReturnType.IsAwaitable(semanticModel, position)
141+
&& method.HasSingleParameter(SpecialType.System_Boolean));
142+
}
127143
}

src/Analyzers/CSharp/Analysis/DisposeResourceAsynchronouslyAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ private static void Analyze(
131131
: context.SemanticModel.GetSymbol(containingMethod, context.CancellationToken)) as IMethodSymbol;
132132

133133
if (methodSymbol?.IsErrorType() == false
134-
&& SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
134+
&& methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, context.Node.SpanStart))
135135
{
136136
ReportDiagnostic(context, usingKeyword);
137137
}

src/Analyzers/CSharp/Analysis/NonAsynchronousMethodNameShouldNotEndWithAsyncAnalyzer.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ public override void Initialize(AnalysisContext context)
3838

3939
context.RegisterCompilationStartAction(startContext =>
4040
{
41-
INamedTypeSymbol asyncAction = startContext.Compilation.GetTypeByMetadataName("Windows.Foundation.IAsyncAction");
42-
43-
bool shouldCheckWindowsRuntimeTypes = asyncAction is not null;
44-
4541
startContext.RegisterSyntaxNodeAction(
4642
c =>
4743
{
@@ -50,14 +46,14 @@ public override void Initialize(AnalysisContext context)
5046
DiagnosticRules.AsynchronousMethodNameShouldEndWithAsync,
5147
DiagnosticRules.NonAsynchronousMethodNameShouldNotEndWithAsync))
5248
{
53-
AnalyzeMethodDeclaration(c, shouldCheckWindowsRuntimeTypes);
49+
AnalyzeMethodDeclaration(c);
5450
}
5551
},
5652
SyntaxKind.MethodDeclaration);
5753
});
5854
}
5955

60-
private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context, bool shouldCheckWindowsRuntimeTypes)
56+
private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
6157
{
6258
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
6359

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

77-
if (SymbolUtility.IsAwaitable(methodSymbol.ReturnType, shouldCheckWindowsRuntimeTypes)
73+
if (methodSymbol.ReturnType.IsAwaitable(context.SemanticModel, methodDeclaration.SpanStart)
7874
|| IsAsyncEnumerableLike(methodSymbol.ReturnType.OriginalDefinition))
7975
{
8076
return;
@@ -105,7 +101,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context,
105101
if (methodSymbol.ImplementsInterfaceMember(allInterfaces: true))
106102
return;
107103

108-
if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType, shouldCheckWindowsRuntimeTypes)
104+
if (!methodSymbol.ReturnType.IsAwaitable(context.SemanticModel, methodDeclaration.SpanStart)
109105
&& !methodSymbol.ReturnType.OriginalDefinition.HasMetadataName(in MetadataNames.System_Collections_Generic_IAsyncEnumerable_T))
110106
{
111107
return;

src/Analyzers/CSharp/Analysis/RemoveRedundantAsyncAwaitAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ void ReportAwaitAndConfigureAwait(AwaitExpressionSyntax awaitExpression)
175175

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

178-
if (typeSymbol?.OriginalDefinition.HasMetadataName(MetadataNames.System_Runtime_CompilerServices_ConfiguredTaskAwaitable_T) == true
178+
if (typeSymbol?.OriginalDefinition.IsAwaitable(context.SemanticModel, expression.SpanStart) == true
179179
&& (expression is InvocationExpressionSyntax invocation))
180180
{
181181
var memberAccess = invocation.Expression as MemberAccessExpressionSyntax;

src/Analyzers/CSharp/Analysis/UseAsyncAwaitAnalyzer.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
5555
if (!body.Statements.Any())
5656
return;
5757

58-
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);
58+
if (context.SemanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken) is not IMethodSymbol methodSymbol)
59+
return;
5960

60-
if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
61+
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
6162
return;
6263

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

82-
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(localFunction, context.CancellationToken);
83+
if (context.SemanticModel.GetDeclaredSymbol(localFunction, context.CancellationToken) is not IMethodSymbol methodSymbol)
84+
return;
8385

84-
if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
86+
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
8587
return;
8688

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

104-
if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
106+
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
105107
return;
106108

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

124-
if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
126+
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
125127
return;
126128

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

146-
if (!SymbolUtility.IsAwaitable(methodSymbol.ReturnType))
148+
if (!methodSymbol.ReturnType.IsAwaitableTaskType(context.SemanticModel, body.SpanStart))
147149
return;
148150

149151
if (IsFixable(body, context))

src/Common/CSharp/Analysis/RemoveAsyncAwaitAnalysis.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ private static bool VerifyTypes(
364364

365365
ITypeSymbol returnType = methodSymbol.ReturnType;
366366

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

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

395395
ITypeSymbol returnType = methodSymbol.ReturnType;
396396

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

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

420-
if (expressionTypeSymbol.OriginalDefinition.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task_T))
420+
if (expressionTypeSymbol.OriginalDefinition.IsAwaitable(semanticModel, expression.SpanStart))
421421
return true;
422422

423423
SimpleMemberInvocationExpressionInfo invocationInfo = SyntaxInfo.SimpleMemberInvocationExpressionInfo(expression);
424424

425425
return invocationInfo.Success
426426
&& invocationInfo.Arguments.Count == 1
427427
&& invocationInfo.NameText == "ConfigureAwait"
428-
&& expressionTypeSymbol.OriginalDefinition.HasMetadataName(MetadataNames.System_Runtime_CompilerServices_ConfiguredTaskAwaitable_T);
428+
&& expressionTypeSymbol.OriginalDefinition.IsAwaitable(semanticModel, expression.SpanStart);
429429
}
430430

431431
private static IMethodSymbol GetMethodSymbol(

0 commit comments

Comments
 (0)