Skip to content

Commit 167b57f

Browse files
committed
Add CancellationTokenAnalyzer
This analyzer ensures that asynchronous methods (whose name ends with Async and return a Task) include a CancellationToken parameter. Fixes #22
1 parent a55758e commit 167b57f

File tree

3 files changed

+76
-5
lines changed

3 files changed

+76
-5
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
namespace OpenStackNetAnalyzers
2+
{
3+
using System.Collections.Immutable;
4+
using System.Linq;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public class CancellationTokenAnalyzer : DiagnosticAnalyzer
10+
{
11+
public const string DiagnosticId = "CancellationToken";
12+
internal const string Title = "Asynchronous methods should accept a CancellationToken";
13+
internal const string MessageFormat = "Asynchronous methods should accept a CancellationToken";
14+
internal const string Category = "OpenStack.Maintainability";
15+
internal const string Description = "Asynchronous methods should accept a CancellationToken";
16+
17+
private static DiagnosticDescriptor Descriptor =
18+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
19+
20+
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
21+
ImmutableArray.Create(Descriptor);
22+
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
24+
{
25+
get
26+
{
27+
return _supportedDiagnostics;
28+
}
29+
}
30+
31+
public override void Initialize(AnalysisContext context)
32+
{
33+
context.RegisterSymbolAction(HandleMethod, SymbolKind.Method);
34+
}
35+
36+
private void HandleMethod(SymbolAnalysisContext context)
37+
{
38+
IMethodSymbol method = (IMethodSymbol)context.Symbol;
39+
if (!method.Name.EndsWith("Async"))
40+
return;
41+
42+
if (!method.ReturnType.IsTask())
43+
return;
44+
45+
foreach (IParameterSymbol parameter in method.Parameters)
46+
{
47+
if (parameter.Type.IsCancellationToken())
48+
return;
49+
}
50+
51+
ImmutableArray<Location> locations = method.Locations;
52+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1)));
53+
}
54+
}
55+
}

OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
</PropertyGroup>
3434
<ItemGroup>
3535
<Compile Include="AssertNullAnalyzer.cs" />
36+
<Compile Include="CancellationTokenAnalyzer.cs" />
3637
<Compile Include="DocumentationSyntaxExtensions.cs" />
3738
<Compile Include="DocumentDelegatingApiCallAnalyzer.cs" />
3839
<Compile Include="DocumentDelegatingApiCallCodeFix.cs" />

OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ internal static class TypeSymbolExtensions
99

1010
private const string FullyQualifiedTask = "global::System.Threading.Tasks.Task";
1111

12+
private const string FullyQualifiedCancellationToken = "global::System.Threading.CancellationToken";
13+
1214
public static bool IsNonNullableValueType(this ITypeSymbol type)
1315
{
1416
if (type == null)
@@ -50,20 +52,33 @@ public static bool IsImmutableArray(this ITypeSymbol type)
5052
return string.Equals(FullyQualifiedImmutableArrayT, originalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal);
5153
}
5254

53-
public static bool IsTask(this INamedTypeSymbol symbol)
55+
public static bool IsTask(this ITypeSymbol symbol)
5456
{
55-
while (symbol != null && symbol.SpecialType != SpecialType.System_Object)
57+
INamedTypeSymbol namedSymbol = symbol as INamedTypeSymbol;
58+
while (namedSymbol != null && namedSymbol.SpecialType != SpecialType.System_Object)
5659
{
57-
if (!symbol.IsGenericType)
60+
if (!namedSymbol.IsGenericType)
5861
{
59-
if (string.Equals(FullyQualifiedTask, symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal))
62+
if (string.Equals(FullyQualifiedTask, namedSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal))
6063
return true;
6164
}
6265

63-
symbol = symbol.BaseType;
66+
namedSymbol = namedSymbol.BaseType;
6467
}
6568

6669
return false;
6770
}
71+
72+
public static bool IsCancellationToken(this ITypeSymbol symbol)
73+
{
74+
INamedTypeSymbol namedSymbol = symbol as INamedTypeSymbol;
75+
if (namedSymbol == null)
76+
return false;
77+
78+
if (namedSymbol.TypeKind != TypeKind.Struct)
79+
return false;
80+
81+
return string.Equals(FullyQualifiedCancellationToken, namedSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal);
82+
}
6883
}
6984
}

0 commit comments

Comments
 (0)