Skip to content
Open
26 changes: 26 additions & 0 deletions docfx/analyzers/VSTHRD003.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ When required to await a task that was started earlier, start it within a delega
`JoinableTaskFactory.RunAsync`, storing the resulting `JoinableTask` in a field or variable.
You can safely await the `JoinableTask` later.

## Suppressing warnings for completed tasks

If you have a property, method, or field that returns a pre-completed task (such as a cached task with a known value),
you can suppress this warning by applying the `[CompletedTask]` attribute to the member.
This attribute is automatically included when you install the `Microsoft.VisualStudio.Threading.Analyzers` package.
Comment thread
drewnoakes marked this conversation as resolved.

```csharp
[Microsoft.VisualStudio.Threading.CompletedTask]
private static readonly Task<bool> TrueTask = Task.FromResult(true);

async Task MyMethodAsync()
{
await TrueTask; // No warning - TrueTask is marked as a completed task
}
```

The analyzer already recognizes the following as safe to await without the attribute:
- `Task.CompletedTask`
- `Task.FromResult(...)`
- `Task.FromCanceled(...)`
- `Task.FromException(...)`
- `TplExtensions.CompletedTask`
- `TplExtensions.CanceledTask`
- `TplExtensions.TrueTask`
- `TplExtensions.FalseTask`

## Simple examples of patterns that are flagged by this analyzer

The following example would likely deadlock if `MyMethod` were called on the main thread,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ public override void Initialize(AnalysisContext context)

private static bool IsSymbolAlwaysOkToAwait(ISymbol? symbol)
{
// Check if the symbol has the CompletedTaskAttribute
if (symbol?.GetAttributes().Any(attr =>
attr.AttributeClass?.Name == Types.CompletedTaskAttribute.TypeName &&
attr.AttributeClass.BelongsToNamespace(Types.CompletedTaskAttribute.Namespace)) == true)
{
return true;
}

if (symbol is IFieldSymbol field)
{
// Allow the TplExtensions.CompletedTask and related fields.
Expand Down Expand Up @@ -288,6 +296,12 @@ private void AnalyzeAwaitExpression(SyntaxNodeAnalysisContext context)

break;
case IMethodSymbol methodSymbol:
// Check if the method itself has the CompletedTaskAttribute
if (IsSymbolAlwaysOkToAwait(methodSymbol))
{
return null;
}

if (Utils.IsTask(methodSymbol.ReturnType) && focusedExpression is InvocationExpressionSyntax invocationExpressionSyntax)
{
// Consider all arguments
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if !COMPLETEDTASKATTRIBUTE_INCLUDED
#define COMPLETEDTASKATTRIBUTE_INCLUDED

namespace Microsoft.VisualStudio.Threading;

/// <summary>
/// Indicates that a property, method, or field returns a task that is already completed.
/// This suppresses VSTHRD003 warnings when awaiting the returned task.
/// </summary>
/// <remarks>
/// Apply this attribute to properties, methods, or fields that return cached, pre-completed tasks
/// such as singleton instances with well-known immutable values.
/// The VSTHRD003 analyzer will not report warnings when these members are awaited,
/// as awaiting an already-completed task does not pose a risk of deadlock.
/// </remarks>
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
#pragma warning disable SA1649 // File name should match first type name
internal sealed class CompletedTaskAttribute : System.Attribute
{
}
#pragma warning restore SA1649 // File name should match first type name
Comment on lines +7 to +49
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses a file-scoped namespace declaration (namespace ...;), which requires C# 10+. Because this file is injected into consuming projects via buildTransitive and compiled, it will fail to compile for consumers using earlier language versions. Consider switching to a namespace block form (namespace ... { ... }) to keep the injected source compatible.

Suggested change
namespace Microsoft.VisualStudio.Threading;
/// <summary>
/// Indicates that a property, method, or field returns a task that is already completed.
/// This suppresses VSTHRD003 warnings when awaiting the returned task.
/// </summary>
/// <remarks>
/// <para>
/// Apply this attribute to properties, methods, or fields that return cached, pre-completed tasks
/// such as singleton instances with well-known immutable values.
/// The VSTHRD003 analyzer will not report warnings when these members are awaited,
/// as awaiting an already-completed task does not pose a risk of deadlock.
/// </para>
/// <para>
/// This attribute can also be applied at the assembly level to mark members in external types
/// that you don't control:
/// <code>
/// [assembly: CompletedTask(Member = "System.Threading.Tasks.TplExtensions.TrueTask")]
/// </code>
/// </para>
/// </remarks>
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field | System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
#pragma warning disable SA1649 // File name should match first type name
internal sealed class CompletedTaskAttribute : System.Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CompletedTaskAttribute"/> class.
/// </summary>
public CompletedTaskAttribute()
{
}
/// <summary>
/// Gets or sets the fully qualified name of the member that returns a completed task.
/// This is only used when the attribute is applied at the assembly level.
/// </summary>
/// <remarks>
/// The format should be: "Namespace.TypeName.MemberName".
/// For example: "System.Threading.Tasks.TplExtensions.TrueTask".
/// </remarks>
public string? Member { get; set; }
}
#pragma warning restore SA1649 // File name should match first type name
namespace Microsoft.VisualStudio.Threading
{
/// <summary>
/// Indicates that a property, method, or field returns a task that is already completed.
/// This suppresses VSTHRD003 warnings when awaiting the returned task.
/// </summary>
/// <remarks>
/// <para>
/// Apply this attribute to properties, methods, or fields that return cached, pre-completed tasks
/// such as singleton instances with well-known immutable values.
/// The VSTHRD003 analyzer will not report warnings when these members are awaited,
/// as awaiting an already-completed task does not pose a risk of deadlock.
/// </para>
/// <para>
/// This attribute can also be applied at the assembly level to mark members in external types
/// that you don't control:
/// <code>
/// [assembly: CompletedTask(Member = "System.Threading.Tasks.TplExtensions.TrueTask")]
/// </code>
/// </para>
/// </remarks>
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field | System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
#pragma warning disable SA1649 // File name should match first type name
internal sealed class CompletedTaskAttribute : System.Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CompletedTaskAttribute"/> class.
/// </summary>
public CompletedTaskAttribute()
{
}
/// <summary>
/// Gets or sets the fully qualified name of the member that returns a completed task.
/// This is only used when the attribute is applied at the assembly level.
/// </summary>
/// <remarks>
/// The format should be: "Namespace.TypeName.MemberName".
/// For example: "System.Threading.Tasks.TplExtensions.TrueTask".
/// </remarks>
public string? Member { get; set; }
}
#pragma warning restore SA1649 // File name should match first type name
}

Copilot uses AI. Check for mistakes.

#endif
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)AdditionalFiles\**">
<AdditionalFiles Include="$(MSBuildThisFileDirectory)AdditionalFiles\*.txt">
<Visible>false</Visible>
</AdditionalFiles>
<Compile Include="$(MSBuildThisFileDirectory)AdditionalFiles\*.cs" Link="%(Filename)%(Extension)">
<Visible>false</Visible>
</Compile>
Comment on lines +7 to +9
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiling buildTransitive/AdditionalFiles/*.cs into consumer projects means these files must be compatible with the consumer's C# language version and nullable settings. Consider adding safeguards (or ensuring the injected source uses broadly-compatible syntax) so referencing the analyzers doesn't introduce unexpected compiler errors/warnings in older projects.

Suggested change
<Compile Include="$(MSBuildThisFileDirectory)AdditionalFiles\*.cs" Link="%(Filename)%(Extension)">
<Visible>false</Visible>
</Compile>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)AdditionalFiles\*.cs" Link="%(Filename)%(Extension)">
<Visible>false</Visible>
</AdditionalFiles>

Copilot uses AI. Check for mistakes.
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions src/Microsoft.VisualStudio.Threading.Analyzers/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,11 @@ public static class TypeLibTypeAttribute

public static readonly ImmutableArray<string> Namespace = Namespaces.SystemRuntimeInteropServices;
}

public static class CompletedTaskAttribute
{
public const string TypeName = "CompletedTaskAttribute";

public static readonly ImmutableArray<string> Namespace = Namespaces.MicrosoftVisualStudioThreading;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,195 @@ static async Task ListenAndWait()
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task DoNotReportWarningWhenAwaitingPropertyWithCompletedTaskAttribute()
{
var test = @"
using System.Threading.Tasks;

namespace Microsoft.VisualStudio.Threading
{
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
internal sealed class CompletedTaskAttribute : System.Attribute
{
}
}

class Tests
{
[Microsoft.VisualStudio.Threading.CompletedTask]
private static Task MyCompletedTask { get; } = Task.CompletedTask;

async Task GetTask()
{
await MyCompletedTask;
}
}
";
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task DoNotReportWarningWhenAwaitingFieldWithCompletedTaskAttribute()
{
var test = @"
using System.Threading.Tasks;

namespace Microsoft.VisualStudio.Threading
{
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
internal sealed class CompletedTaskAttribute : System.Attribute
{
}
}

class Tests
{
[Microsoft.VisualStudio.Threading.CompletedTask]
private static readonly Task MyCompletedTask = Task.CompletedTask;

async Task GetTask()
{
await MyCompletedTask;
}
}
";
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task DoNotReportWarningWhenAwaitingMethodWithCompletedTaskAttribute()
{
var test = @"
using System.Threading.Tasks;

namespace Microsoft.VisualStudio.Threading
{
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
internal sealed class CompletedTaskAttribute : System.Attribute
{
}
}

class Tests
{
[Microsoft.VisualStudio.Threading.CompletedTask]
private static Task GetCompletedTask() => Task.CompletedTask;

async Task TestMethod()
{
await GetCompletedTask();
}
}
";
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task DoNotReportWarningWhenReturningPropertyWithCompletedTaskAttribute()
{
var test = @"
using System.Threading.Tasks;

namespace Microsoft.VisualStudio.Threading
{
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
internal sealed class CompletedTaskAttribute : System.Attribute
{
}
}

class Tests
{
[Microsoft.VisualStudio.Threading.CompletedTask]
private static Task MyCompletedTask { get; } = Task.CompletedTask;

Task GetTask() => MyCompletedTask;
}
";
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task ReportWarningWhenAwaitingPropertyWithoutCompletedTaskAttribute()
{
var test = @"
using System.Threading.Tasks;

class Tests
{
private static Task MyTask { get; } = Task.Run(() => {});

async Task GetTask()
{
await [|MyTask|];
}
}
";
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task DoNotReportWarningWhenAwaitingTaskGenericPropertyWithCompletedTaskAttribute()
{
var test = @"
using System.Threading.Tasks;

namespace Microsoft.VisualStudio.Threading
{
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
internal sealed class CompletedTaskAttribute : System.Attribute
{
}
}

class Tests
{
[Microsoft.VisualStudio.Threading.CompletedTask]
private static Task<int> MyCompletedTask { get; } = Task.FromResult(42);

async Task<int> GetResult()
{
return await MyCompletedTask;
}
}
";
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task DoNotReportWarningWhenAwaitingPropertyWithCompletedTaskAttributeInJtfRun()
{
var test = @"
using System.Threading.Tasks;
using Microsoft.VisualStudio.Threading;

namespace Microsoft.VisualStudio.Threading
{
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
internal sealed class CompletedTaskAttribute : System.Attribute
{
}
}

class Tests
{
[Microsoft.VisualStudio.Threading.CompletedTask]
private static Task MyCompletedTask { get; } = Task.CompletedTask;

void TestMethod()
{
JoinableTaskFactory jtf = null;
jtf.Run(async delegate
{
await MyCompletedTask;
});
}
}
";
await CSVerify.VerifyAnalyzerAsync(test);
}

private DiagnosticResult CreateDiagnostic(int line, int column, int length) =>
CSVerify.Diagnostic().WithSpan(line, column, line, column + length);
}
Comment on lines +2108 to 2158
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three tests appear to be duplicates of earlier cases in this same file (e.g., the non-readonly field / public setter / internal setter attribute-misuse tests already exist above). Keeping both copies increases test runtime and maintenance cost; consider removing the redundant duplicates or consolidating into a single set of cases.

Suggested change
class Tests
{
[{|#0:Microsoft.VisualStudio.Threading.CompletedTask|}]
public static Task MyTask { get; set; } = Task.CompletedTask; // Public setter
async Task GetTask()
{
await [|MyTask|];
}
}
";
DiagnosticResult expected = new DiagnosticResult(Microsoft.VisualStudio.Threading.Analyzers.VSTHRD003UseJtfRunAsyncAnalyzer.InvalidAttributeUseDescriptor)
.WithLocation(0)
.WithArguments("Properties must not have non-private setters.");
await CSVerify.VerifyAnalyzerAsync(test, expected);
}
[Fact]
public async Task ReportDiagnosticWhenCompletedTaskAttributeOnPropertyWithInternalSetterWithDiagnosticOnAttribute()
{
var test = @"
using System.Threading.Tasks;
namespace Microsoft.VisualStudio.Threading
{
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
internal sealed class CompletedTaskAttribute : System.Attribute
{
}
}
class Tests
{
[{|#0:Microsoft.VisualStudio.Threading.CompletedTask|}]
public static Task MyTask { get; internal set; } = Task.CompletedTask; // Internal setter
async Task GetTask()
{
await [|MyTask|];
}
}
";
DiagnosticResult expected = new DiagnosticResult(Microsoft.VisualStudio.Threading.Analyzers.VSTHRD003UseJtfRunAsyncAnalyzer.InvalidAttributeUseDescriptor)
.WithLocation(0)
.WithArguments("Properties must not have non-private setters.");
await CSVerify.VerifyAnalyzerAsync(test, expected);
}
private DiagnosticResult CreateDiagnostic(int line, int column, int length) =>
CSVerify.Diagnostic().WithSpan(line, column, line, column + length);
}
private DiagnosticResult CreateDiagnostic(int line, int column, int length) =>
CSVerify.Diagnostic().WithSpan(line, column, line, column + length);
}

Copilot uses AI. Check for mistakes.
Loading