|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Completion.Providers;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Recommendations;
/// <summary>
/// The <see cref="AwaitCompletionProvider"/> adds async modifier if the return type is Task or ValueTask. The tests
/// here are only checking whether the completion item is provided or not. Tests for checking adding async modifier are
/// in: src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests_AwaitCompletion.vb
/// </summary>
[Trait(Traits.Feature, Traits.Features.Completion)]
public sealed class AwaitCompletionProviderTests : AbstractCSharpCompletionProviderTests
{
internal override Type GetCompletionProviderType() => typeof(AwaitCompletionProvider);
private const string CompletionDisplayTextAwait = "await";
private const string CompletionDisplayTextAwaitAndConfigureAwait = "awaitf";
private async Task VerifyAbsenceAsync([StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string code)
{
await VerifyItemIsAbsentAsync(code, CompletionDisplayTextAwait);
await VerifyItemIsAbsentAsync(code, CompletionDisplayTextAwaitAndConfigureAwait);
}
private async Task VerifyAbsenceAsync(
[StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string code, LanguageVersion languageVersion = LanguageVersion.Default)
{
await VerifyItemIsAbsentAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwait);
await VerifyItemIsAbsentAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwaitAndConfigureAwait);
}
private async Task VerifyKeywordAsync(
[StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string code, LanguageVersion languageVersion = LanguageVersion.Default, string? inlineDescription = null, bool dotAwait = false, bool dotAwaitf = false)
{
var expectedDescription = dotAwait
? GetDescription(CompletionDisplayTextAwait, FeaturesResources.Await_the_preceding_expression)
: GetDescription(CompletionDisplayTextAwait, FeaturesResources.Asynchronously_waits_for_the_task_to_finish);
await VerifyItemExistsAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwait, glyph: (int)Glyph.Keyword, expectedDescriptionOrNull: expectedDescription, inlineDescription: inlineDescription);
if (dotAwaitf)
{
expectedDescription = string.Format(FeaturesResources.Await_the_preceding_expression_and_add_ConfigureAwait_0, "false");
await VerifyItemExistsAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwaitAndConfigureAwait, glyph: (int)Glyph.Keyword, expectedDescriptionOrNull: expectedDescription, inlineDescription: inlineDescription);
}
else
{
await VerifyItemIsAbsentAsync(GetMarkup(code, languageVersion), CompletionDisplayTextAwaitAndConfigureAwait);
}
static string GetDescription(string keyword, string tooltip)
=> $"{string.Format(FeaturesResources._0_Keyword, keyword)}\r\n{tooltip}";
}
[Fact]
public async Task TestNotInTypeContext()
{
await VerifyAbsenceAsync("""
class Program
{
$$
}
""");
}
[Fact]
public async Task TestStatementInMethod()
{
await VerifyKeywordAsync("""
class C
{
void F()
{
$$ }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestStatementInMethod_Async()
{
await VerifyKeywordAsync("""
class C
{
async Task F()
{
$$ }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestStatementInMethod_TopLevel()
{
await VerifyKeywordAsync("$$", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestExpressionInAsyncMethod()
{
await VerifyKeywordAsync("""
class C
{
async Task F()
{
var z = $$ }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestExpressionInNonAsyncMethodWithTaskReturn()
{
await VerifyKeywordAsync("""
class C
{
Task F()
{
var z = $$ }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestExpressionInAsyncMethod_TopLevel()
{
await VerifyKeywordAsync("var z = $$", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestUsingStatement()
{
await VerifyAbsenceAsync("""
class C
{
async Task F()
{
using $$ }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestUsingStatement_TopLevel()
{
await VerifyAbsenceAsync("using $$", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestUsingDirective()
=> await VerifyAbsenceAsync("using $$");
[Fact]
public async Task TestGlobalUsingDirective()
=> await VerifyAbsenceAsync("global using $$");
[Fact]
public async Task TestForeachStatement()
{
await VerifyAbsenceAsync("""
class C
{
async Task F()
{
foreach $$ }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestForeachStatement_TopLevel()
{
await VerifyAbsenceAsync("foreach $$", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestNotInQuery()
{
await VerifyAbsenceAsync("""
class C
{
async Task F()
{
var z = from a in "char"
select $$ }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestNotInQuery_TopLevel()
{
await VerifyAbsenceAsync(
"""
var z = from a in "char"
select $$
""", LanguageVersion.CSharp9);
}
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/907052")]
public async Task TestInFinally()
{
await VerifyKeywordAsync("""
class C
{
async Task F()
{
try { }
finally { $$ } }
}
""", LanguageVersion.CSharp9);
}
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/907052")]
public async Task TestInFinally_TopLevel()
{
await VerifyKeywordAsync(
"""
try { }
finally { $$ }
""", LanguageVersion.CSharp9);
}
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/907052")]
public async Task TestInCatch()
{
await VerifyKeywordAsync("""
class C
{
async Task F()
{
try { }
catch { $$ } }
}
""", LanguageVersion.CSharp9);
}
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/907052")]
public async Task TestInCatch_TopLevel()
{
await VerifyKeywordAsync(
"""
try { }
catch { $$ }
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestNotInLock()
{
await VerifyAbsenceAsync("""
class C
{
async Task F()
{
lock(this) { $$ } }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestNotInLock_TopLevel()
{
await VerifyAbsenceAsync("lock (this) { $$ }", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestInAsyncLambdaInCatch()
{
await VerifyKeywordAsync("""
class C
{
async Task F()
{
try { }
catch { var z = async () => $$ } }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestInAsyncLambdaInCatch_TopLevel()
{
await VerifyKeywordAsync(
"""
try { }
catch { var z = async () => $$ }
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestAwaitInLock()
{
await VerifyKeywordAsync("""
class C
{
async Task F()
{
lock($$ }
}
""", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestAwaitInLock_TopLevel()
{
await VerifyKeywordAsync("lock($$", LanguageVersion.CSharp9);
}
[Fact]
public async Task TestDotAwaitSuggestAfterDotOnTask()
{
await VerifyKeywordAsync("""
using System.Threading.Tasks;
class C
{
async Task F(Task someTask)
{
someTask.$$
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact]
public async Task TestDotAwaitSuggestAfterDotOnTaskOfT()
{
await VerifyKeywordAsync("""
using System.Threading.Tasks;
class C
{
async Task F(Task<int> someTask)
{
someTask.$$
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact]
public async Task TestDotAwaitSuggestAfterDotOnValueTask()
{
var valueTaskAssembly = typeof(ValueTask).Assembly.Location;
var markup = $$"""
<Workspace>
<Project Language="C#" AssemblyName="Assembly1" CommonReferences="true">
<MetadataReference>{{valueTaskAssembly}}</MetadataReference>
<Document FilePath="Test2.cs">
using System.Threading.Tasks;
class C
{
async Task F(ValueTask someTask)
{
someTask.$$
}
}
</Document>
</Project>
</Workspace>
""";
await VerifyItemExistsAsync(markup, "await");
await VerifyItemExistsAsync(markup, "awaitf");
}
[Fact]
public async Task TestDotAwaitSuggestAfterDotOnCustomAwaitable()
{
await VerifyKeywordAsync("""
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
public class DummyAwaiter: INotifyCompletion {
public bool IsCompleted => true;
public void OnCompleted(Action continuation) => continuation();
public void GetResult() {}
}
public class CustomAwaitable
{
public DummyAwaiter GetAwaiter() => new DummyAwaiter();
}
static class Program
{
static async Task Main()
{
var awaitable = new CustomAwaitable();
awaitable.$$;
}
}
""", dotAwait: true);
}
[Fact]
public async Task TestDotAwaitSuggestAfterDotOnCustomAwaitableButNotConfigureAwaitEvenIfPresent()
{
await VerifyKeywordAsync("""
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
public class DummyAwaiter: INotifyCompletion {
public bool IsCompleted => true;
public void OnCompleted(Action continuation) => continuation();
public void GetResult() {}
}
public class CustomAwaitable
{
public DummyAwaiter GetAwaiter() => new DummyAwaiter();
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) => default;
}
static class Program
{
static async Task Main()
{
var awaitable = new CustomAwaitable();
awaitable.$$;
}
}
""", dotAwait: true, dotAwaitf: false);
}
[Fact]
public async Task TestDotAwaitSuggestAfterDotDot()
{
await VerifyKeywordAsync("""
using System.Threading.Tasks;
static class Program
{
static async Task Main(Task someTask)
{
someTask.$$.;
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact]
public async Task TestDotAwaitSuggestAfterDotBeforeType()
{
await VerifyKeywordAsync("""
using System;
using System.Threading.Tasks;
static class Program
{
static async Task Main(Task someTask)
{
someTask.$$
Int32 i = 0;
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact]
public async Task TestDotAwaitSuggestAfterDotBeforeAnotherAwait()
{
await VerifyKeywordAsync("""
using System;
using System.Threading.Tasks;
static class Program
{
static async Task Main(Task someTask)
{
someTask.$$
await Test();
}
async Task Test() { }
}
""", dotAwait: true, dotAwaitf: true);
}
[Theory]
[InlineData("")]
[InlineData("X.Y Test();")]
[InlineData("var x;")]
[InlineData("int x;")]
[InlineData("System.Int32 x;")]
[InlineData("if (true) { }")]
[InlineData("System.Int32 Test() => 0;")]
[InlineData("async Task<System.Int32> Test() => await Task.FromResult(1);")]
public async Task TestDotAwaitSuggestAfterDotBeforeDifferentStatements(string statement)
{
await VerifyKeywordAsync($$"""
using System;
using System.Threading.Tasks;
static class Program
{
static async Task Main(Task someTask)
{
someTask.$$
{{statement}}
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Theory]
// static
[InlineData("StaticField.$$")]
[InlineData("StaticProperty.$$")]
[InlineData("StaticMethod().$$")]
// parameters, locals and local function
[InlineData("local.$$")]
[InlineData("parameter.$$")]
[InlineData("LocalFunction().$$")]
// members
[InlineData("c.Field.$$")]
[InlineData("c.Property.$$")]
[InlineData("c.Method().$$")]
[InlineData("c.Self.Field.$$")]
[InlineData("c.Self.Property.$$")]
[InlineData("c.Self.Method().$$")]
[InlineData("c.Function()().$$")]
// indexer, operator, conversion
[InlineData("c[0].$$")]
[InlineData("(c + c).$$")]
[InlineData("((Task)c).$$")]
[InlineData("(c as Task).$$")]
// parenthesized
[InlineData("(parameter).$$")]
[InlineData("((parameter)).$$")]
[InlineData("(true ? parameter : parameter).$$")]
[InlineData("(null ?? Task.CompletedTask).$$")]
public async Task TestDotAwaitSuggestAfterDifferentExpressions(string expression)
{
await VerifyKeywordAsync($$"""
using System;
using System.Threading.Tasks;
class C
{
public C Self => this;
public Task Field = Task.CompletedTask;
public Task Method() => Task.CompletedTask;
public Task Property => Task.CompletedTask;
public Task this[int i] => Task.CompletedTask;
public Func<Task> Function() => () => Task.CompletedTask;
public static Task operator +(C left, C right) => Task.CompletedTask;
public static explicit operator Task(C c) => Task.CompletedTask;
}
static class Program
{
static Task StaticField = Task.CompletedTask;
static Task StaticProperty => Task.CompletedTask;
static Task StaticMethod() => Task.CompletedTask;
static async Task Main(Task parameter)
{
Task local = Task.CompletedTask;
var c = new C();
{{expression}}
Task LocalFunction() => Task.CompletedTask;
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact(Skip = "Fails because speculative binding can't figure out that local is a Task.")]
[WorkItem("https://github.com/dotnet/roslyn/issues/56245")]
public async Task TestDotAwaitSuggestBeforeLocalFunction()
{
// Speculative binding a local as expression finds the local as ILocalSymbol, but the type is ErrorType.
// This is only the case when
// * await is partially written (local.a),
// * only for locals (e.g. IParameterSymbols are fine) which
// * are declared with var
// * The return type of the local function is used as first name in a MemberAccess in the declarator
await VerifyKeywordAsync("""
using System.Threading.Tasks;
static class Program
{
static async Task Main()
{
var local = Task.CompletedTask;
local.a$$
Task LocalFunction() => Task.CompletedTask;
}
}
""");
}
[Theory]
[InlineData("await Task.Run(async () => Task.CompletedTask.$$")]
[InlineData("await Task.Run(async () => someTask.$$")]
[InlineData("await Task.Run(async () => someTask.$$);")]
[InlineData("await Task.Run(async () => { someTask.$$ }")]
[InlineData("await Task.Run(async () => { someTask.$$ });")]
[InlineData("Task.Run(async () => await someTask).$$")]
[InlineData("await Task.Run(() => someTask.$$")]
public async Task TestDotAwaitSuggestInLambdas(string lambda)
{
await VerifyKeywordAsync($$"""
using System.Threading.Tasks;
static class Program
{
static async Task Main()
{
var someTask = Task.CompletedTask;
{{lambda}}
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact]
public async Task TestDotAwaitNotAfterDotOnTaskIfAlreadyAwaited()
{
await VerifyAbsenceAsync("""
using System.Threading.Tasks;
class C
{
async Task F(Task someTask)
{
await someTask.$$
}
}
""");
}
[Fact]
public async Task TestDotAwaitNotAfterTaskType()
{
await VerifyAbsenceAsync("""
using System.Threading.Tasks;
class C
{
async Task F()
{
Task.$$
}
}
""");
}
[Fact]
public async Task TestDotAwaitNotInLock()
{
await VerifyAbsenceAsync("""
using System.Threading.Tasks;
class C
{
async Task F(Task someTask)
{
lock(this) { someTask.$$ }
}
}
""");
}
[Fact]
public async Task TestDotAwaitNotInLock_TopLevel()
{
await VerifyAbsenceAsync("""
using System.Threading.Tasks;
lock(this) { Task.CompletedTask.$$ }
""");
}
[Fact]
public async Task TestDotAwaitQueryNotInSelect()
{
await VerifyAbsenceAsync("""
using System.Linq;
using System.Threading.Tasks;
class C
{
async Task F()
{
var z = from t in new[] { Task.CompletedTask }
select t.$$
}
}
""");
}
[Fact]
public async Task TestDotAwaitQueryInFirstFromClause()
{
await VerifyKeywordAsync("""
using System.Linq;
using System.Threading.Tasks;
class C
{
async Task F()
{
var arrayTask2 = Task.FromResult(new int[0]);
var z = from t in arrayTask2.$$
select t;
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact]
public async Task TestDotAwaitQueryNotInSecondFromClause()
{
await VerifyNoItemsExistAsync("""
using System.Linq;
using System.Threading.Tasks;
class C
{
async Task F()
{
var array1 = new int[0];
var arrayTask2 = Task.FromResult(new int[0]);
var z = from i1 in array1
from i2 in arrayTask2.$$
select i2;
}
}
""");
}
[Fact]
public async Task TestDotAwaitQueryNotInContinuation()
{
await VerifyNoItemsExistAsync("""
using System.Linq;
using System.Threading.Tasks;
class C
{
async Task F()
{
var array1 = new int[0];
var arrayTask2 = Task.FromResult(new int[0]);
var z = from i1 in array1
select i1 into c
from i2 in arrayTask2.$$
select i2;
}
}
""");
}
[Fact]
public async Task TestDotAwaitQueryInJoinClause()
{
await VerifyKeywordAsync("""
using System.Linq;
using System.Threading.Tasks;
class C
{
async Task F()
{
var array1 = new int[0];
var arrayTask2 = Task.FromResult(new int[0]);
var z = from i1 in array1
join i2 in arrayTask2.$$ on i1 equals i2
select i1;
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact]
public async Task TestDotAwaitQueryInJoinIntoClause()
{
await VerifyKeywordAsync("""
using System.Linq;
using System.Threading.Tasks;
class C
{
async Task F()
{
var array1 = new int[0];
var arrayTask2 = Task.FromResult(new int[0]);
var z = from i1 in array1
join i2 in arrayTask2.$$ on i1 equals i2 into g
select g;
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Fact]
public async Task TestDotAwaitNotAfterConditionalAccessOfTaskMembers()
{
// The conditional access suggests, that someTask can be null.
// await on null throws at runtime, so the user should do
// if (someTask is not null) await someTask;
// or
// await (someTask ?? Task.CompletedTask)
// Completion should not offer await, because the patterns above would change to much code.
// This decision should be revised after https://github.com/dotnet/csharplang/issues/35
// is implemented.
await VerifyAbsenceAsync("""
using System.Threading.Tasks;
class C
{
async Task F(Task someTask)
{
someTask?.$$
}
}
""");
}
[Theory]
[InlineData("c?.SomeTask.$$")]
[InlineData("c.M()?.SomeTask.$$")]
[InlineData("c.Pro?.SomeTask.$$")]
[InlineData("c?.M().SomeTask.$$")]
[InlineData("c?.Pro.SomeTask.$$")]
[InlineData("c?.M()?.SomeTask.$$")]
[InlineData("c?.Pro?.SomeTask.$$")]
[InlineData("c.M()?.Pro.SomeTask.$$")]
[InlineData("c.Pro?.M().SomeTask.$$")]
[InlineData("c.M()?.M().M()?.M().SomeTask.$$")]
[InlineData("new C().M()?.Pro.M()?.M().SomeTask.$$")]
public async Task TestDotAwaitNotAfterDotInConditionalAccessChain(string conditionalAccess)
{
await VerifyAbsenceAsync($$"""
using System.Threading.Tasks;
public class C
{
public Task SomeTask => Task.CompletedTask;
public C Pro => this;
public C M() => this;
}
static class Program
{
public static async Task Main()
{
var c = new C();
{{conditionalAccess}}
}
}
""");
}
[Theory]
[InlineData("c!.SomeTask.$$")]
[InlineData("c.SomeTask!.$$")]
[InlineData("c.M()!.SomeTask.$$")]
[InlineData("c.Pro!.SomeTask.$$")]
[InlineData("c!.M().SomeTask.$$")]
[InlineData("c!.Pro.SomeTask.$$")]
[InlineData("c!.M()!.SomeTask.$$")]
[InlineData("c!.Pro!.SomeTask.$$")]
[InlineData("c.M()!.Pro.SomeTask.$$")]
[InlineData("c.Pro!.M().SomeTask.$$")]
[InlineData("c.M()!.M().M()!.M().SomeTask.$$")]
[InlineData("new C().M()!.Pro.M()!.M().SomeTask.$$")]
public async Task TestDotAwaitAfterNullForgivingOperatorAccessChain(string nullForgivingAccess)
{
await VerifyKeywordAsync($$"""
#nullable enable
using System.Threading.Tasks;
public class C
{
public Task? SomeTask => Task.CompletedTask;
public C? Pro => this;
public C? M() => this;
}
static class Program
{
public static async Task Main(params string[] args)
{
var c = args[1] == string.Empty ? new C() : null;
{{nullForgivingAccess}}
}
}
""", dotAwait: true, dotAwaitf: true);
}
[Theory, CombinatorialData]
[WorkItem("https://github.com/dotnet/roslyn/issues/58921")]
public async Task TestInCastExpressionThatMightBeParenthesizedExpression(bool hasNewline)
{
var code = $$"""
class C
{
void M()
{
var data = (n$$) {{(hasNewline ? Environment.NewLine : string.Empty)}} M();
}
}
""";
if (hasNewline)
await VerifyKeywordAsync(code);
else
await VerifyAbsenceAsync(code);
}
}
|