Skip to content

Commit 92f45aa

Browse files
committed
Merge pull request #23 from sharwell/fix-15
Asynchronous interface checks
2 parents 21251cf + 167b57f commit 92f45aa

File tree

6 files changed

+226
-0
lines changed

6 files changed

+226
-0
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: 3 additions & 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" />
@@ -49,6 +50,8 @@
4950
<Compile Include="PlaceholderDocumentationCodeFix.cs" />
5051
<Compile Include="Properties\AssemblyInfo.cs" />
5152
<Compile Include="SdkTypeSymbolExtensions.cs" />
53+
<Compile Include="ServiceMethodPrepareAsyncAnalyzer.cs" />
54+
<Compile Include="ServiceMethodReturnValueAnalyzer.cs" />
5255
<Compile Include="SpacingExtensions.cs" />
5356
<Compile Include="TypeSymbolExtensions.cs" />
5457
<Compile Include="XmlSyntaxFactory.cs" />

OpenStackNetAnalyzers/OpenStackNetAnalyzers/SdkTypeSymbolExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ internal static class SdkTypeSymbolExtensions
99

1010
private const string FullyQualifiedDelegatingHttpApiCallT = "global::OpenStack.Net.DelegatingHttpApiCall<T>";
1111

12+
private const string FullyQualifiedIHttpService = "global::OpenStack.Services.IHttpService";
13+
1214
public static bool IsExtensibleJsonObject(this INamedTypeSymbol symbol)
1315
{
1416
while (symbol != null && symbol.SpecialType != SpecialType.System_Object)
@@ -39,5 +41,19 @@ public static bool IsDelegatingHttpApiCall(this INamedTypeSymbol symbol)
3941

4042
return false;
4143
}
44+
45+
public static bool IsHttpServiceInterface(this INamedTypeSymbol symbol)
46+
{
47+
if (symbol == null || symbol.TypeKind != TypeKind.Interface)
48+
return false;
49+
50+
foreach (INamedTypeSymbol interfaceType in symbol.AllInterfaces)
51+
{
52+
if (string.Equals(FullyQualifiedIHttpService, interfaceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
53+
return true;
54+
}
55+
56+
return false;
57+
}
4258
}
4359
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
namespace OpenStackNetAnalyzers
2+
{
3+
using System;
4+
using System.Collections.Immutable;
5+
using System.Linq;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public class ServiceMethodPrepareAsyncAnalyzer : DiagnosticAnalyzer
11+
{
12+
public const string DiagnosticId = "ServiceMethodPrepareAsync";
13+
internal const string Title = "Service methods should be named Prepare{Name}Async";
14+
internal const string MessageFormat = "Service methods should be named Prepare{Name}Async";
15+
internal const string Category = "OpenStack.Maintainability";
16+
internal const string Description = "Service methods should be named Prepare{Name}Async";
17+
18+
private static DiagnosticDescriptor Descriptor =
19+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
20+
21+
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
22+
ImmutableArray.Create(Descriptor);
23+
24+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
25+
{
26+
get
27+
{
28+
return _supportedDiagnostics;
29+
}
30+
}
31+
32+
public override void Initialize(AnalysisContext context)
33+
{
34+
context.RegisterSymbolAction(HandleNamedType, SymbolKind.NamedType);
35+
}
36+
37+
private void HandleNamedType(SymbolAnalysisContext context)
38+
{
39+
INamedTypeSymbol symbol = (INamedTypeSymbol)context.Symbol;
40+
if (!symbol.IsHttpServiceInterface())
41+
return;
42+
43+
foreach (IMethodSymbol method in symbol.GetMembers().OfType<IMethodSymbol>())
44+
{
45+
if (string.IsNullOrEmpty(method.Name))
46+
continue;
47+
48+
if (method.Name.StartsWith("Prepare", StringComparison.Ordinal) && method.Name.EndsWith("Async", StringComparison.Ordinal))
49+
{
50+
// TODO check letter following 'Prepare'
51+
continue;
52+
}
53+
54+
ImmutableArray<Location> locations = method.Locations;
55+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1)));
56+
}
57+
}
58+
}
59+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 ServiceMethodReturnValueAnalyzer : DiagnosticAnalyzer
10+
{
11+
public const string DiagnosticId = "ServiceMethodReturnValue";
12+
internal const string Title = "Service interface methods should return a Task<T> with a result that implements IHttpApiCall<T>";
13+
internal const string MessageFormat = "Service interface methods should return a Task<T> with a result that implements IHttpApiCall<T>";
14+
internal const string Category = "OpenStack.Maintainability";
15+
internal const string Description = "Service interface methods should return a Task<T> with a result that implements IHttpApiCall<T>";
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(HandleNamedType, SymbolKind.NamedType);
34+
}
35+
36+
private void HandleNamedType(SymbolAnalysisContext context)
37+
{
38+
INamedTypeSymbol symbol = (INamedTypeSymbol)context.Symbol;
39+
if (!symbol.IsHttpServiceInterface())
40+
return;
41+
42+
foreach (IMethodSymbol method in symbol.GetMembers().OfType<IMethodSymbol>())
43+
{
44+
INamedTypeSymbol returnType = method.ReturnType as INamedTypeSymbol;
45+
if (returnType.IsTask() && returnType.IsGenericType && returnType.TypeArguments.Length == 1)
46+
{
47+
INamedTypeSymbol genericArgument = returnType.TypeArguments[0] as INamedTypeSymbol;
48+
if (genericArgument.IsDelegatingHttpApiCall())
49+
{
50+
// the method returns the expected type
51+
continue;
52+
}
53+
}
54+
55+
ImmutableArray<Location> locations = method.Locations;
56+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1)));
57+
}
58+
}
59+
}
60+
}

OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ internal static class TypeSymbolExtensions
77
{
88
private const string FullyQualifiedImmutableArrayT = "global::System.Collections.Immutable.ImmutableArray<T>";
99

10+
private const string FullyQualifiedTask = "global::System.Threading.Tasks.Task";
11+
12+
private const string FullyQualifiedCancellationToken = "global::System.Threading.CancellationToken";
13+
1014
public static bool IsNonNullableValueType(this ITypeSymbol type)
1115
{
1216
if (type == null)
@@ -47,5 +51,34 @@ public static bool IsImmutableArray(this ITypeSymbol type)
4751

4852
return string.Equals(FullyQualifiedImmutableArrayT, originalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal);
4953
}
54+
55+
public static bool IsTask(this ITypeSymbol symbol)
56+
{
57+
INamedTypeSymbol namedSymbol = symbol as INamedTypeSymbol;
58+
while (namedSymbol != null && namedSymbol.SpecialType != SpecialType.System_Object)
59+
{
60+
if (!namedSymbol.IsGenericType)
61+
{
62+
if (string.Equals(FullyQualifiedTask, namedSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal))
63+
return true;
64+
}
65+
66+
namedSymbol = namedSymbol.BaseType;
67+
}
68+
69+
return false;
70+
}
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+
}
5083
}
5184
}

0 commit comments

Comments
 (0)