Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace OpenStackNetAnalyzers
{
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CancellationTokenAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "CancellationToken";
internal const string Title = "Asynchronous methods should accept a CancellationToken";
internal const string MessageFormat = "Asynchronous methods should accept a CancellationToken";
internal const string Category = "OpenStack.Maintainability";
internal const string Description = "Asynchronous methods should accept a CancellationToken";

private static DiagnosticDescriptor Descriptor =
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);

private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
ImmutableArray.Create(Descriptor);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(HandleMethod, SymbolKind.Method);
}

private void HandleMethod(SymbolAnalysisContext context)
{
IMethodSymbol method = (IMethodSymbol)context.Symbol;
if (!method.Name.EndsWith("Async"))
return;

if (!method.ReturnType.IsTask())
return;

foreach (IParameterSymbol parameter in method.Parameters)
{
if (parameter.Type.IsCancellationToken())
return;
}

ImmutableArray<Location> locations = method.Locations;
context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="AssertNullAnalyzer.cs" />
<Compile Include="CancellationTokenAnalyzer.cs" />
<Compile Include="DocumentationSyntaxExtensions.cs" />
<Compile Include="DocumentDelegatingApiCallAnalyzer.cs" />
<Compile Include="DocumentDelegatingApiCallCodeFix.cs" />
Expand All @@ -49,6 +50,8 @@
<Compile Include="PlaceholderDocumentationCodeFix.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SdkTypeSymbolExtensions.cs" />
<Compile Include="ServiceMethodPrepareAsyncAnalyzer.cs" />
<Compile Include="ServiceMethodReturnValueAnalyzer.cs" />
<Compile Include="SpacingExtensions.cs" />
<Compile Include="TypeSymbolExtensions.cs" />
<Compile Include="XmlSyntaxFactory.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ internal static class SdkTypeSymbolExtensions

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

private const string FullyQualifiedIHttpService = "global::OpenStack.Services.IHttpService";

public static bool IsExtensibleJsonObject(this INamedTypeSymbol symbol)
{
while (symbol != null && symbol.SpecialType != SpecialType.System_Object)
Expand Down Expand Up @@ -39,5 +41,19 @@ public static bool IsDelegatingHttpApiCall(this INamedTypeSymbol symbol)

return false;
}

public static bool IsHttpServiceInterface(this INamedTypeSymbol symbol)
{
if (symbol == null || symbol.TypeKind != TypeKind.Interface)
return false;

foreach (INamedTypeSymbol interfaceType in symbol.AllInterfaces)
{
if (string.Equals(FullyQualifiedIHttpService, interfaceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
return true;
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace OpenStackNetAnalyzers
{
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ServiceMethodPrepareAsyncAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "ServiceMethodPrepareAsync";
internal const string Title = "Service methods should be named Prepare{Name}Async";
internal const string MessageFormat = "Service methods should be named Prepare{Name}Async";
internal const string Category = "OpenStack.Maintainability";
internal const string Description = "Service methods should be named Prepare{Name}Async";

private static DiagnosticDescriptor Descriptor =
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);

private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
ImmutableArray.Create(Descriptor);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(HandleNamedType, SymbolKind.NamedType);
}

private void HandleNamedType(SymbolAnalysisContext context)
{
INamedTypeSymbol symbol = (INamedTypeSymbol)context.Symbol;
if (!symbol.IsHttpServiceInterface())
return;

foreach (IMethodSymbol method in symbol.GetMembers().OfType<IMethodSymbol>())
{
if (string.IsNullOrEmpty(method.Name))
continue;

if (method.Name.StartsWith("Prepare", StringComparison.Ordinal) && method.Name.EndsWith("Async", StringComparison.Ordinal))
{
// TODO check letter following 'Prepare'
continue;
}

ImmutableArray<Location> locations = method.Locations;
context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1)));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace OpenStackNetAnalyzers
{
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ServiceMethodReturnValueAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "ServiceMethodReturnValue";
internal const string Title = "Service interface methods should return a Task<T> with a result that implements IHttpApiCall<T>";
internal const string MessageFormat = "Service interface methods should return a Task<T> with a result that implements IHttpApiCall<T>";
internal const string Category = "OpenStack.Maintainability";
internal const string Description = "Service interface methods should return a Task<T> with a result that implements IHttpApiCall<T>";

private static DiagnosticDescriptor Descriptor =
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);

private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
ImmutableArray.Create(Descriptor);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(HandleNamedType, SymbolKind.NamedType);
}

private void HandleNamedType(SymbolAnalysisContext context)
{
INamedTypeSymbol symbol = (INamedTypeSymbol)context.Symbol;
if (!symbol.IsHttpServiceInterface())
return;

foreach (IMethodSymbol method in symbol.GetMembers().OfType<IMethodSymbol>())
{
INamedTypeSymbol returnType = method.ReturnType as INamedTypeSymbol;
if (returnType.IsTask() && returnType.IsGenericType && returnType.TypeArguments.Length == 1)
{
INamedTypeSymbol genericArgument = returnType.TypeArguments[0] as INamedTypeSymbol;
if (genericArgument.IsDelegatingHttpApiCall())
{
// the method returns the expected type
continue;
}
}

ImmutableArray<Location> locations = method.Locations;
context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1)));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ internal static class TypeSymbolExtensions
{
private const string FullyQualifiedImmutableArrayT = "global::System.Collections.Immutable.ImmutableArray<T>";

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

private const string FullyQualifiedCancellationToken = "global::System.Threading.CancellationToken";

public static bool IsNonNullableValueType(this ITypeSymbol type)
{
if (type == null)
Expand Down Expand Up @@ -47,5 +51,34 @@ public static bool IsImmutableArray(this ITypeSymbol type)

return string.Equals(FullyQualifiedImmutableArrayT, originalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal);
}

public static bool IsTask(this ITypeSymbol symbol)
{
INamedTypeSymbol namedSymbol = symbol as INamedTypeSymbol;
while (namedSymbol != null && namedSymbol.SpecialType != SpecialType.System_Object)
{
if (!namedSymbol.IsGenericType)
{
if (string.Equals(FullyQualifiedTask, namedSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal))
return true;
}

namedSymbol = namedSymbol.BaseType;
}

return false;
}

public static bool IsCancellationToken(this ITypeSymbol symbol)
{
INamedTypeSymbol namedSymbol = symbol as INamedTypeSymbol;
if (namedSymbol == null)
return false;

if (namedSymbol.TypeKind != TypeKind.Struct)
return false;

return string.Equals(FullyQualifiedCancellationToken, namedSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal);
}
}
}