diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/CancellationTokenAnalyzer.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/CancellationTokenAnalyzer.cs new file mode 100644 index 0000000..964c4e0 --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/CancellationTokenAnalyzer.cs @@ -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 _supportedDiagnostics = + ImmutableArray.Create(Descriptor); + + public override ImmutableArray 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 locations = method.Locations; + context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1))); + } + } +} diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj index 189be22..5463e40 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj @@ -33,6 +33,7 @@ + @@ -49,6 +50,8 @@ + + diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/SdkTypeSymbolExtensions.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/SdkTypeSymbolExtensions.cs index 61becd3..59578a3 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/SdkTypeSymbolExtensions.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/SdkTypeSymbolExtensions.cs @@ -9,6 +9,8 @@ internal static class SdkTypeSymbolExtensions private const string FullyQualifiedDelegatingHttpApiCallT = "global::OpenStack.Net.DelegatingHttpApiCall"; + private const string FullyQualifiedIHttpService = "global::OpenStack.Services.IHttpService"; + public static bool IsExtensibleJsonObject(this INamedTypeSymbol symbol) { while (symbol != null && symbol.SpecialType != SpecialType.System_Object) @@ -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; + } } } diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/ServiceMethodPrepareAsyncAnalyzer.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/ServiceMethodPrepareAsyncAnalyzer.cs new file mode 100644 index 0000000..2097936 --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/ServiceMethodPrepareAsyncAnalyzer.cs @@ -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 _supportedDiagnostics = + ImmutableArray.Create(Descriptor); + + public override ImmutableArray 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()) + { + 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 locations = method.Locations; + context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1))); + } + } + } +} diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/ServiceMethodReturnValueAnalyzer.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/ServiceMethodReturnValueAnalyzer.cs new file mode 100644 index 0000000..33aba5d --- /dev/null +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/ServiceMethodReturnValueAnalyzer.cs @@ -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 with a result that implements IHttpApiCall"; + internal const string MessageFormat = "Service interface methods should return a Task with a result that implements IHttpApiCall"; + internal const string Category = "OpenStack.Maintainability"; + internal const string Description = "Service interface methods should return a Task with a result that implements IHttpApiCall"; + + private static DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); + + private static readonly ImmutableArray _supportedDiagnostics = + ImmutableArray.Create(Descriptor); + + public override ImmutableArray 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()) + { + 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 locations = method.Locations; + context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1))); + } + } + } +} diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs index 62040da..453d6a5 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs @@ -7,6 +7,10 @@ internal static class TypeSymbolExtensions { private const string FullyQualifiedImmutableArrayT = "global::System.Collections.Immutable.ImmutableArray"; + 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) @@ -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); + } } }