Skip to content

Commit bc6cf84

Browse files
Suggest proper 'Async'-suffixed name for Task-returning symbols (#79518)
Fixes #17989
2 parents 07faf00 + 5804e6b commit bc6cf84

File tree

5 files changed

+60
-51
lines changed

5 files changed

+60
-51
lines changed

src/EditorFeatures/CSharpTest/Completion/CompletionProviders/DeclarationNameCompletionProviderTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,9 +380,10 @@ async Task<C> $$
380380
}
381381
""", "GetCAsync");
382382

383-
[Fact(Skip = "not yet implemented")]
383+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17989")]
384384
public Task NonAsyncTaskOfT()
385385
=> VerifyItemExistsAsync("""
386+
using System.Threading.Tasks;
386387
public class C
387388
{
388389
Task<C> $$

src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public override async Task ProvideCompletionsAsync(CompletionContext completionC
7373
var sortValue = 0;
7474
foreach (var recommender in Recommenders)
7575
{
76-
var names = await recommender.Value.ProvideRecommendedNamesAsync(completionContext, document, context, nameInfo, cancellationToken).ConfigureAwait(false);
76+
var names = await recommender.Value.ProvideRecommendedNamesAsync(
77+
completionContext, document, context, nameInfo, cancellationToken).ConfigureAwait(false);
7778

7879
foreach (var (name, glyph) in names)
7980
{

src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameInfo.cs

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ private static bool IsPropertyDeclaration(SyntaxToken token, SemanticModel seman
198198
return result.Type != null;
199199
}
200200

201-
private static bool IsMethodDeclaration(SyntaxToken token, SemanticModel semanticModel,
202-
CancellationToken cancellationToken, out NameDeclarationInfo result)
201+
private static bool IsMethodDeclaration(
202+
SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken, out NameDeclarationInfo result)
203203
{
204204
result = IsLastTokenOfType<MethodDeclarationSyntax>(
205205
token,
@@ -220,45 +220,30 @@ private static NameDeclarationInfo IsFollowingTypeOrComma<TSyntaxNode>(
220220
Func<DeclarationModifiers, ImmutableArray<SymbolKindOrTypeKind>> possibleDeclarationComputer,
221221
CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
222222
{
223-
if (!IsPossibleTypeToken(token) && !token.IsKind(SyntaxKind.CommaToken))
224-
{
223+
var afterComma = token.IsKind(SyntaxKind.CommaToken);
224+
if (!IsPossibleTypeToken(token) && !afterComma)
225225
return default;
226-
}
227226

228227
var target = token.GetAncestor<TSyntaxNode>();
229228
if (target == null)
230-
{
231229
return default;
232-
}
233230

234-
if (token.IsKind(SyntaxKind.CommaToken) && token.Parent != target)
235-
{
231+
if (afterComma && token.Parent != target)
236232
return default;
237-
}
238233

239234
var typeSyntax = typeSyntaxGetter(target);
240235
if (typeSyntax == null)
241-
{
242236
return default;
243-
}
244237

245-
if (!token.IsKind(SyntaxKind.CommaToken) && token != typeSyntax.GetLastToken())
246-
{
238+
if (!afterComma && token != typeSyntax.GetLastToken())
247239
return default;
248-
}
249240

250241
var modifiers = modifierGetter(target);
251242
if (modifiers == null)
252-
{
253243
return default;
254-
}
255244

256-
return new NameDeclarationInfo(
257-
possibleDeclarationComputer(GetDeclarationModifiers(modifiers.Value)),
258-
GetAccessibility(modifiers.Value),
259-
GetDeclarationModifiers(modifiers.Value),
260-
semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type,
261-
semanticModel.GetAliasInfo(typeSyntax, cancellationToken));
245+
return ComputeInfo(
246+
semanticModel, possibleDeclarationComputer, typeSyntax, modifiers.Value, tryInferAsync: !afterComma, cancellationToken);
262247
}
263248

264249
private static NameDeclarationInfo IsLastTokenOfType<TSyntaxNode>(
@@ -295,11 +280,36 @@ private static NameDeclarationInfo IsLastTokenOfType<TSyntaxNode>(
295280

296281
var modifiers = modifierGetter(target);
297282

283+
return ComputeInfo(semanticModel, possibleDeclarationComputer, typeSyntax, modifiers, tryInferAsync: true, cancellationToken);
284+
}
285+
286+
private static NameDeclarationInfo ComputeInfo(
287+
SemanticModel semanticModel,
288+
Func<DeclarationModifiers, ImmutableArray<SymbolKindOrTypeKind>> possibleDeclarationComputer,
289+
SyntaxNode typeSyntax,
290+
SyntaxTokenList modifiers,
291+
bool tryInferAsync,
292+
CancellationToken cancellationToken)
293+
{
294+
var declarationModifiers = GetDeclarationModifiers(modifiers);
295+
var possibleDeclarations = possibleDeclarationComputer(declarationModifiers);
296+
297+
// Treat a declaration as async if if it is explicitly marked as 'async' or if it is a method that returns a
298+
// Task-like type. The latter ensures that even if the user didn't have an explicit 'async' modifier, we still
299+
// name the member in an appropriate fashion (e.g. `GetCustomerAsync` for `public Task<Customer> $$`).
300+
var type = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type;
301+
if (!declarationModifiers.IsAsync && tryInferAsync)
302+
{
303+
var knownTaskTypes = new KnownTaskTypes(semanticModel.Compilation);
304+
if (knownTaskTypes.IsTaskLike(type))
305+
declarationModifiers = declarationModifiers.WithAsync(true);
306+
}
307+
298308
return new NameDeclarationInfo(
299-
possibleDeclarationComputer(GetDeclarationModifiers(modifiers)),
309+
possibleDeclarations,
300310
GetAccessibility(modifiers),
301-
GetDeclarationModifiers(modifiers),
302-
semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type,
311+
declarationModifiers,
312+
type,
303313
semanticModel.GetAliasInfo(typeSyntax, cancellationToken));
304314
}
305315

src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameRecommender.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -215,22 +215,14 @@ private static void GetRecommendedNames(
215215

216216
foreach (var kind in declarationInfo.PossibleSymbolKinds)
217217
{
218-
ProcessRules(rules, firstMatchOnly: true, kind, baseNames, declarationInfo, context, result, semanticFactsService, seenBaseNames, seenUniqueNames, cancellationToken);
219-
ProcessRules(supplementaryRules, firstMatchOnly: false, kind, baseNames, declarationInfo, context, result, semanticFactsService, seenBaseNames, seenUniqueNames, cancellationToken);
218+
ProcessRules(rules, firstMatchOnly: true, kind);
219+
ProcessRules(supplementaryRules, firstMatchOnly: false, kind);
220220
}
221221

222-
static void ProcessRules(
222+
void ProcessRules(
223223
ImmutableArray<NamingRule> rules,
224224
bool firstMatchOnly,
225-
SymbolSpecification.SymbolKindOrTypeKind kind,
226-
ImmutableArray<ImmutableArray<string>> baseNames,
227-
NameDeclarationInfo declarationInfo,
228-
CSharpSyntaxContext context,
229-
ArrayBuilder<(string, Glyph)> result,
230-
ISemanticFactsService semanticFactsService,
231-
PooledHashSet<string> seenBaseNames,
232-
PooledHashSet<string> seenUniqueNames,
233-
CancellationToken cancellationToken)
225+
SymbolSpecification.SymbolKindOrTypeKind kind)
234226
{
235227
var modifiers = declarationInfo.Modifiers;
236228
foreach (var rule in rules)

src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/KnownTypes.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Diagnostics.CodeAnalysis;
6+
57
namespace Microsoft.CodeAnalysis.Shared.Extensions;
68

79
internal readonly struct KnownTaskTypes(Compilation compilation)
@@ -17,22 +19,25 @@ internal readonly struct KnownTaskTypes(Compilation compilation)
1719
public readonly INamedTypeSymbol? IAsyncEnumerableOfTType = compilation.IAsyncEnumerableOfTType();
1820
public readonly INamedTypeSymbol? IAsyncEnumeratorOfTType = compilation.IAsyncEnumeratorOfTType();
1921

20-
public bool IsTaskLike(ITypeSymbol returnType)
22+
public bool IsTaskLike([NotNullWhen(true)] ITypeSymbol? returnType)
2123
{
22-
if (returnType.Equals(this.TaskType))
23-
return true;
24+
if (returnType is not null)
25+
{
26+
if (returnType.Equals(this.TaskType))
27+
return true;
2428

25-
if (returnType.Equals(this.ValueTaskType))
26-
return true;
29+
if (returnType.Equals(this.ValueTaskType))
30+
return true;
2731

28-
if (returnType.OriginalDefinition.Equals(this.TaskOfTType))
29-
return true;
32+
if (returnType.OriginalDefinition.Equals(this.TaskOfTType))
33+
return true;
3034

31-
if (returnType.OriginalDefinition.Equals(this.ValueTaskOfTType))
32-
return true;
35+
if (returnType.OriginalDefinition.Equals(this.ValueTaskOfTType))
36+
return true;
3337

34-
if (returnType.IsErrorType())
35-
return returnType.Name is "Task" or "ValueTask";
38+
if (returnType.IsErrorType())
39+
return returnType.Name is "Task" or "ValueTask";
40+
}
3641

3742
return false;
3843
}

0 commit comments

Comments
 (0)