Skip to content

Commit d067b66

Browse files
Copilotdrewnoakes
andcommitted
Add assembly-level CompletedTaskAttribute support for external types
Co-authored-by: drewnoakes <[email protected]>
1 parent a864039 commit d067b66

File tree

4 files changed

+319
-8
lines changed

4 files changed

+319
-8
lines changed

docfx/analyzers/VSTHRD003.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ async Task MyMethodAsync()
2626
}
2727
```
2828

29+
### Marking external types
30+
31+
You can also apply the attribute at the assembly level to mark members in external types that you don't control:
32+
33+
```csharp
34+
[assembly: Microsoft.VisualStudio.Threading.CompletedTask(Member = "ExternalLibrary.ExternalClass.CompletedTaskProperty")]
35+
```
36+
37+
This is useful when you're using third-party libraries that have pre-completed tasks but aren't annotated with the attribute.
38+
The `Member` property should contain the fully qualified name of the member in the format `Namespace.TypeName.MemberName`.
39+
2940
The analyzer already recognizes the following as safe to await without the attribute:
3041
- `Task.CompletedTask`
3142
- `Task.FromResult(...)`

src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD003UseJtfRunAsyncAnalyzer.cs

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,42 @@ public override void Initialize(AnalysisContext context)
7171
context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeLambdaExpression), SyntaxKind.ParenthesizedLambdaExpression);
7272
}
7373

74-
private static bool IsSymbolAlwaysOkToAwait(ISymbol? symbol)
74+
private static bool IsSymbolAlwaysOkToAwait(ISymbol? symbol, Compilation compilation)
7575
{
76-
// Check if the symbol has the CompletedTaskAttribute
77-
if (symbol?.GetAttributes().Any(attr =>
76+
if (symbol is null)
77+
{
78+
return false;
79+
}
80+
81+
// Check if the symbol has the CompletedTaskAttribute directly applied
82+
if (symbol.GetAttributes().Any(attr =>
7883
attr.AttributeClass?.Name == Types.CompletedTaskAttribute.TypeName &&
79-
attr.AttributeClass.BelongsToNamespace(Types.CompletedTaskAttribute.Namespace)) == true)
84+
attr.AttributeClass.BelongsToNamespace(Types.CompletedTaskAttribute.Namespace)))
8085
{
8186
return true;
8287
}
8388

89+
// Check for assembly-level CompletedTaskAttribute
90+
foreach (AttributeData assemblyAttr in compilation.Assembly.GetAttributes())
91+
{
92+
if (assemblyAttr.AttributeClass?.Name == Types.CompletedTaskAttribute.TypeName &&
93+
assemblyAttr.AttributeClass.BelongsToNamespace(Types.CompletedTaskAttribute.Namespace))
94+
{
95+
// Look for the Member named argument
96+
foreach (KeyValuePair<string, TypedConstant> namedArg in assemblyAttr.NamedArguments)
97+
{
98+
if (namedArg.Key == "Member" && namedArg.Value.Value is string memberName)
99+
{
100+
// Check if this symbol matches the specified member name
101+
if (IsSymbolMatchingMemberName(symbol, memberName))
102+
{
103+
return true;
104+
}
105+
}
106+
}
107+
}
108+
}
109+
84110
if (symbol is IFieldSymbol field)
85111
{
86112
// Allow the TplExtensions.CompletedTask and related fields.
@@ -103,6 +129,45 @@ private static bool IsSymbolAlwaysOkToAwait(ISymbol? symbol)
103129
return false;
104130
}
105131

132+
private static bool IsSymbolMatchingMemberName(ISymbol symbol, string memberName)
133+
{
134+
// Build the fully qualified name of the symbol
135+
string fullyQualifiedName = GetFullyQualifiedName(symbol);
136+
137+
// Compare with the member name (case-sensitive)
138+
return string.Equals(fullyQualifiedName, memberName, StringComparison.Ordinal);
139+
}
140+
141+
private static string GetFullyQualifiedName(ISymbol symbol)
142+
{
143+
if (symbol.ContainingType is null)
144+
{
145+
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
146+
}
147+
148+
// For members (properties, fields, methods), construct: Namespace.TypeName.MemberName
149+
List<string> parts = new List<string>();
150+
151+
// Add member name
152+
parts.Add(symbol.Name);
153+
154+
// Add containing type hierarchy
155+
INamedTypeSymbol? currentType = symbol.ContainingType;
156+
while (currentType is not null)
157+
{
158+
parts.Insert(0, currentType.Name);
159+
currentType = currentType.ContainingType;
160+
}
161+
162+
// Add namespace
163+
if (symbol.ContainingNamespace is not null && !symbol.ContainingNamespace.IsGlobalNamespace)
164+
{
165+
parts.Insert(0, symbol.ContainingNamespace.ToDisplayString());
166+
}
167+
168+
return string.Join(".", parts);
169+
}
170+
106171
private void AnalyzeArrowExpressionClause(SyntaxNodeAnalysisContext context)
107172
{
108173
var arrowExpressionClause = (ArrowExpressionClauseSyntax)context.Node;
@@ -191,7 +256,7 @@ private void AnalyzeAwaitExpression(SyntaxNodeAnalysisContext context)
191256
symbolType = localSymbol.Type;
192257
dataflowAnalysisCompatibleVariable = true;
193258
break;
194-
case IPropertySymbol propertySymbol when !IsSymbolAlwaysOkToAwait(propertySymbol):
259+
case IPropertySymbol propertySymbol when !IsSymbolAlwaysOkToAwait(propertySymbol, context.Compilation):
195260
symbolType = propertySymbol.Type;
196261

197262
if (focusedExpression is MemberAccessExpressionSyntax memberAccessExpression)
@@ -285,7 +350,7 @@ private void AnalyzeAwaitExpression(SyntaxNodeAnalysisContext context)
285350
}
286351

287352
ISymbol? definition = declarationSemanticModel.GetSymbolInfo(memberAccessSyntax, cancellationToken).Symbol;
288-
if (IsSymbolAlwaysOkToAwait(definition))
353+
if (IsSymbolAlwaysOkToAwait(definition, context.Compilation))
289354
{
290355
return null;
291356
}
@@ -297,7 +362,7 @@ private void AnalyzeAwaitExpression(SyntaxNodeAnalysisContext context)
297362
break;
298363
case IMethodSymbol methodSymbol:
299364
// Check if the method itself has the CompletedTaskAttribute
300-
if (IsSymbolAlwaysOkToAwait(methodSymbol))
365+
if (IsSymbolAlwaysOkToAwait(methodSymbol, context.Compilation))
301366
{
302367
return null;
303368
}

src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/CompletedTaskAttribute.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,40 @@ namespace Microsoft.VisualStudio.Threading;
1111
/// This suppresses VSTHRD003 warnings when awaiting the returned task.
1212
/// </summary>
1313
/// <remarks>
14+
/// <para>
1415
/// Apply this attribute to properties, methods, or fields that return cached, pre-completed tasks
1516
/// such as singleton instances with well-known immutable values.
1617
/// The VSTHRD003 analyzer will not report warnings when these members are awaited,
1718
/// as awaiting an already-completed task does not pose a risk of deadlock.
19+
/// </para>
20+
/// <para>
21+
/// This attribute can also be applied at the assembly level to mark members in external types
22+
/// that you don't control:
23+
/// <code>
24+
/// [assembly: CompletedTask(Member = "System.Threading.Tasks.TplExtensions.TrueTask")]
25+
/// </code>
26+
/// </para>
1827
/// </remarks>
19-
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
28+
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field | System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
2029
#pragma warning disable SA1649 // File name should match first type name
2130
internal sealed class CompletedTaskAttribute : System.Attribute
2231
{
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="CompletedTaskAttribute"/> class.
34+
/// </summary>
35+
public CompletedTaskAttribute()
36+
{
37+
}
38+
39+
/// <summary>
40+
/// Gets or sets the fully qualified name of the member that returns a completed task.
41+
/// This is only used when the attribute is applied at the assembly level.
42+
/// </summary>
43+
/// <remarks>
44+
/// The format should be: "Namespace.TypeName.MemberName".
45+
/// For example: "System.Threading.Tasks.TplExtensions.TrueTask".
46+
/// </remarks>
47+
public string? Member { get; set; }
2348
}
2449
#pragma warning restore SA1649 // File name should match first type name
2550

test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD003UseJtfRunAsyncAnalyzerTests.cs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,6 +1642,216 @@ void TestMethod()
16421642
await CSVerify.VerifyAnalyzerAsync(test);
16431643
}
16441644

1645+
[Fact]
1646+
public async Task DoNotReportWarningWhenAwaitingPropertyMarkedByAssemblyLevelAttribute()
1647+
{
1648+
var test = @"
1649+
using System.Threading.Tasks;
1650+
1651+
[assembly: Microsoft.VisualStudio.Threading.CompletedTask(Member = ""ExternalLibrary.ExternalClass.CompletedTaskProperty"")]
1652+
1653+
namespace Microsoft.VisualStudio.Threading
1654+
{
1655+
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field | System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
1656+
internal sealed class CompletedTaskAttribute : System.Attribute
1657+
{
1658+
public CompletedTaskAttribute() { }
1659+
public string? Member { get; set; }
1660+
}
1661+
}
1662+
1663+
namespace ExternalLibrary
1664+
{
1665+
public static class ExternalClass
1666+
{
1667+
public static Task CompletedTaskProperty { get; } = Task.CompletedTask;
1668+
}
1669+
}
1670+
1671+
class Tests
1672+
{
1673+
async Task TestMethod()
1674+
{
1675+
await ExternalLibrary.ExternalClass.CompletedTaskProperty;
1676+
}
1677+
}
1678+
";
1679+
await CSVerify.VerifyAnalyzerAsync(test);
1680+
}
1681+
1682+
[Fact]
1683+
public async Task DoNotReportWarningWhenAwaitingFieldMarkedByAssemblyLevelAttribute()
1684+
{
1685+
var test = @"
1686+
using System.Threading.Tasks;
1687+
1688+
[assembly: Microsoft.VisualStudio.Threading.CompletedTask(Member = ""ExternalLibrary.ExternalClass.CompletedTaskField"")]
1689+
1690+
namespace Microsoft.VisualStudio.Threading
1691+
{
1692+
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field | System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
1693+
internal sealed class CompletedTaskAttribute : System.Attribute
1694+
{
1695+
public CompletedTaskAttribute() { }
1696+
public string? Member { get; set; }
1697+
}
1698+
}
1699+
1700+
namespace ExternalLibrary
1701+
{
1702+
public static class ExternalClass
1703+
{
1704+
public static readonly Task CompletedTaskField = Task.FromResult(true);
1705+
}
1706+
}
1707+
1708+
class Tests
1709+
{
1710+
async Task TestMethod()
1711+
{
1712+
await ExternalLibrary.ExternalClass.CompletedTaskField;
1713+
}
1714+
}
1715+
";
1716+
await CSVerify.VerifyAnalyzerAsync(test);
1717+
}
1718+
1719+
[Fact]
1720+
public async Task DoNotReportWarningWhenAwaitingMethodMarkedByAssemblyLevelAttribute()
1721+
{
1722+
var test = @"
1723+
using System.Threading.Tasks;
1724+
1725+
[assembly: Microsoft.VisualStudio.Threading.CompletedTask(Member = ""ExternalLibrary.ExternalClass.GetCompletedTask"")]
1726+
1727+
namespace Microsoft.VisualStudio.Threading
1728+
{
1729+
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field | System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
1730+
internal sealed class CompletedTaskAttribute : System.Attribute
1731+
{
1732+
public CompletedTaskAttribute() { }
1733+
public string? Member { get; set; }
1734+
}
1735+
}
1736+
1737+
namespace ExternalLibrary
1738+
{
1739+
public static class ExternalClass
1740+
{
1741+
public static Task GetCompletedTask() => Task.CompletedTask;
1742+
}
1743+
}
1744+
1745+
class Tests
1746+
{
1747+
async Task TestMethod()
1748+
{
1749+
await ExternalLibrary.ExternalClass.GetCompletedTask();
1750+
}
1751+
}
1752+
";
1753+
await CSVerify.VerifyAnalyzerAsync(test);
1754+
}
1755+
1756+
[Fact]
1757+
public async Task DoNotReportWarningWhenReturningPropertyMarkedByAssemblyLevelAttribute()
1758+
{
1759+
var test = @"
1760+
using System.Threading.Tasks;
1761+
1762+
[assembly: Microsoft.VisualStudio.Threading.CompletedTask(Member = ""ExternalLibrary.ExternalClass.CompletedTaskProperty"")]
1763+
1764+
namespace Microsoft.VisualStudio.Threading
1765+
{
1766+
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field | System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
1767+
internal sealed class CompletedTaskAttribute : System.Attribute
1768+
{
1769+
public CompletedTaskAttribute() { }
1770+
public string? Member { get; set; }
1771+
}
1772+
}
1773+
1774+
namespace ExternalLibrary
1775+
{
1776+
public static class ExternalClass
1777+
{
1778+
public static Task CompletedTaskProperty { get; } = Task.CompletedTask;
1779+
}
1780+
}
1781+
1782+
class Tests
1783+
{
1784+
Task GetTask() => ExternalLibrary.ExternalClass.CompletedTaskProperty;
1785+
}
1786+
";
1787+
await CSVerify.VerifyAnalyzerAsync(test);
1788+
}
1789+
1790+
[Fact]
1791+
public async Task ReportWarningWhenAwaitingPropertyNotMarkedByAssemblyLevelAttribute()
1792+
{
1793+
var test = @"
1794+
using System.Threading.Tasks;
1795+
1796+
namespace ExternalLibrary
1797+
{
1798+
public static class ExternalClass
1799+
{
1800+
public static Task SomeTaskProperty { get; } = Task.Run(() => {});
1801+
}
1802+
}
1803+
1804+
class Tests
1805+
{
1806+
async Task TestMethod()
1807+
{
1808+
await [|ExternalLibrary.ExternalClass.SomeTaskProperty|];
1809+
}
1810+
}
1811+
";
1812+
await CSVerify.VerifyAnalyzerAsync(test);
1813+
}
1814+
1815+
[Fact]
1816+
public async Task DoNotReportWarningWithMultipleAssemblyLevelAttributes()
1817+
{
1818+
var test = @"
1819+
using System.Threading.Tasks;
1820+
1821+
[assembly: Microsoft.VisualStudio.Threading.CompletedTask(Member = ""ExternalLibrary.ExternalClass.Task1"")]
1822+
[assembly: Microsoft.VisualStudio.Threading.CompletedTask(Member = ""ExternalLibrary.ExternalClass.Task2"")]
1823+
1824+
namespace Microsoft.VisualStudio.Threading
1825+
{
1826+
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Method | System.AttributeTargets.Field | System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
1827+
internal sealed class CompletedTaskAttribute : System.Attribute
1828+
{
1829+
public CompletedTaskAttribute() { }
1830+
public string? Member { get; set; }
1831+
}
1832+
}
1833+
1834+
namespace ExternalLibrary
1835+
{
1836+
public static class ExternalClass
1837+
{
1838+
public static Task Task1 { get; } = Task.CompletedTask;
1839+
public static Task Task2 { get; } = Task.FromResult(true);
1840+
}
1841+
}
1842+
1843+
class Tests
1844+
{
1845+
async Task TestMethod()
1846+
{
1847+
await ExternalLibrary.ExternalClass.Task1;
1848+
await ExternalLibrary.ExternalClass.Task2;
1849+
}
1850+
}
1851+
";
1852+
await CSVerify.VerifyAnalyzerAsync(test);
1853+
}
1854+
16451855
private DiagnosticResult CreateDiagnostic(int line, int column, int length) =>
16461856
CSVerify.Diagnostic().WithSpan(line, column, line, column + length);
16471857
}

0 commit comments

Comments
 (0)