Skip to content

Commit

Permalink
Add VSTHRD110: Observe result of async calls
Browse files Browse the repository at this point in the history
  • Loading branch information
AArnott committed Jun 5, 2018
1 parent 433e69e commit 6d39c3c
Show file tree
Hide file tree
Showing 20 changed files with 448 additions and 2 deletions.
75 changes: 75 additions & 0 deletions doc/analyzers/VSTHRD110.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# VSTHRD110 Observe result of async calls

Tasks returned from async methods should be awaited, or assigned to a variable for observation later.
Methods that return `Task`s often complete and report their work via the `Task` they return, and simply
invoking the method does not guarantee that its work is complete nor successful. Using the `await` keyword
just before the method call causes execution of the calling method to effectively suspend until the called
method has completed and rethrows any exception thrown by the method.

When a `Task` or `Task<T>` is returned and is not awaited or redirected in some other way,
within the context of a synchronous method, a warning is reported.

This rule does *not* apply to calls made within async methods, since [CS4014][CS4014] already reports these.

## Examples of patterns that are flagged by this analyzer

```csharp
void Foo() {
DoStuffAsync();
}

async Task DoStuffAsync() { /* ... */ }
```

## Solution

Convert the method to be async and await the expression:

```csharp
async Task FooAsync() {
await DoStuffAsync();
}

async Task DoStuffAsync() { /* ... */ }
```

When the calling method's signature cannot be changed, wrap the method body in a `JoinableTaskFactory.Run` delegate instead:

```csharp
void Foo() {
jtf.Run(async delegate {
await DoStuffAsync();
});
}

async Task DoStuffAsync() { /* ... */ }
```

One other option is to assign the result of the method call to a field or local variable, presumably to track it later:

```csharp
void Foo() {
Task watchThis = DoStuffAsync();
}

async Task DoStuffAsync() { /* ... */ }
```

When tracking the `Task` with a field, remember that to await it later without risk of deadlocking,
wrap it in a `JoinableTask` using `JoinableTaskFactory.RunAsync`, per [the 3rd rule](../threading_rules.md#Rule3).

```csharp
JoinableTask watchThis;

void Foo() {
this.watchThis = jtf.RunAsync(() => DoStuffAsync());
}

async Task WaitForFooToFinishAsync() {
await this.watchThis;
}

async Task DoStuffAsync() { /* ... */ }
```

[CS4014]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs4014
1 change: 1 addition & 0 deletions doc/analyzers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ID | Title | Severity | Supports
[VSTHRD107](VSTHRD107.md) | Await Task within using expression | Advisory
[VSTHRD108](VSTHRD108.md) | Assert thread affinity unconditionally | Advisory | [1st rule](../threading_rules.md#Rule1), [VSTHRD010](VSTHRD010.md)
[VSTHRD109](VSTHRD109.md) | Switch instead of assert in async methods | Advisory | [1st rule](../threading_rules.md#Rule1)
[VSTHRD110](VSTHRD110.md) | Observe result of async calls | Advisory
[VSTHRD200](VSTHRD200.md) | Use `Async` naming convention | Guideline | [VSTHRD103](VSTHRD103.md)

## Severity descriptions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
namespace Microsoft.VisualStudio.Threading.Analyzers.Tests
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis;
using Xunit;
using Xunit.Abstractions;

public class VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests : DiagnosticVerifier
{
private DiagnosticResult expect = new DiagnosticResult
{
Id = VSTHRD110ObserveResultOfAsyncCallsAnalyzer.Id,
Severity = DiagnosticSeverity.Warning,
};

public VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests(ITestOutputHelper logger)
: base(logger)
{
}

protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new VSTHRD110ObserveResultOfAsyncCallsAnalyzer();

[Fact]
public void SyncMethod_ProducesDiagnostic()
{
var test = @"
using System.Threading.Tasks;
class Test {
void Foo()
{
BarAsync();
}
Task BarAsync() => null;
}
";

this.expect = this.CreateDiagnostic(7, 9, 8);
this.VerifyCSharpDiagnostic(test, this.expect);
}

[Fact]
public void SyncDelegateWithinAsyncMethod_ProducesDiagnostic()
{
var test = @"
using System.Threading.Tasks;
class Test {
async Task Foo()
{
await Task.Run(delegate {
BarAsync();
});
}
Task BarAsync() => null;
}
";

this.expect = this.CreateDiagnostic(8, 13, 8);
this.VerifyCSharpDiagnostic(test, this.expect);
}

[Fact]
public void AssignToLocal_ProducesNoDiagnostic()
{
var test = @"
using System.Threading.Tasks;
class Test {
void Foo()
{
Task t = BarAsync();
}
Task BarAsync() => null;
}
";

this.VerifyCSharpDiagnostic(test);
}

[Fact]
public void ForgetExtension_ProducesNoDiagnostic()
{
var test = @"
using System.Threading.Tasks;
using Microsoft.VisualStudio.Threading;
class Test {
void Foo()
{
BarAsync().Forget();
}
Task BarAsync() => null;
}
";

this.VerifyCSharpDiagnostic(test);
}

[Fact]
public void AssignToField_ProducesNoDiagnostic()
{
var test = @"
using System.Threading.Tasks;
class Test {
Task t;
void Foo()
{
this.t = BarAsync();
}
Task BarAsync() => null;
}
";

this.VerifyCSharpDiagnostic(test);
}

[Fact]
public void PassToOtherMethod_ProducesNoDiagnostic()
{
var test = @"
using System.Threading.Tasks;
class Test {
void Foo()
{
OtherMethod(BarAsync());
}
Task BarAsync() => null;
void OtherMethod(Task t) { }
}
";

this.VerifyCSharpDiagnostic(test);
}

[Fact]
public void AsyncMethod_ProducesNoDiagnostic()
{
var test = @"
using System.Threading.Tasks;
class Test {
async Task FooAsync()
{
BarAsync();
}
Task BarAsync() => null;
}
";

this.VerifyCSharpDiagnostic(test); // CS4014 should already take care of this case.
}

private DiagnosticResult CreateDiagnostic(int line, int column, int length, string messagePattern = null) =>
new DiagnosticResult
{
Id = this.expect.Id,
MessagePattern = messagePattern ?? this.expect.MessagePattern,
Severity = this.expect.Severity,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", line, column, line, column + length) },
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ Použijte raději AsyncLazy&lt;T&gt;.</target>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ Verwenden Sie stattdessen AsyncLazy&lt;T&gt;.</target>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ Use AsyncLazy&lt;T&gt; en su lugar.</target>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ Utilisez AsyncLazy&lt;T&gt; à la place</target>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ In alternativa, usare AsyncLazy&lt;T&gt;.</target>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ Use AsyncLazy&lt;T&gt; instead.</source>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ Use AsyncLazy&lt;T&gt; instead.</source>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ Zamiast tego używaj klasy AsyncLazy&lt;T&gt;.</target>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ Em vez disso, use AsyncLazy&lt;T&gt;.</target>
<target state="new">Await SwitchToMainThreadAsync</target>
<note from="MultilingualBuild" annotates="source" priority="2">Do not translate either of these. The first is a keyword, the second is a method name.</note>
</trans-unit>
<trans-unit id="VSTHRD110_MessageFormat" translate="yes" xml:space="preserve">
<source>Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</source>
<target state="new">Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.</target>
</trans-unit>
<trans-unit id="VSTHRD110_Title" translate="yes" xml:space="preserve">
<source>Observe result of async calls</source>
<target state="new">Observe result of async calls</target>
</trans-unit>
</group>
</body>
</file>
Expand Down
Loading

0 comments on commit 6d39c3c

Please sign in to comment.