From a8ab402e29decae6199c6cef5a305d870fa5c145 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 6 Feb 2015 11:42:16 -0600 Subject: [PATCH 1/3] Add ServiceMethodPrepareAsyncAnalyzer Fixes #15 --- .../OpenStackNetAnalyzers.csproj | 1 + .../SdkTypeSymbolExtensions.cs | 16 +++++ .../ServiceMethodPrepareAsyncAnalyzer.cs | 59 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 OpenStackNetAnalyzers/OpenStackNetAnalyzers/ServiceMethodPrepareAsyncAnalyzer.cs diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj index 189be22..4f5adcd 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj @@ -49,6 +49,7 @@ + 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))); + } + } + } +} From a55758e30a77b98f753516d73562ce65de8521dc Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 6 Feb 2015 11:42:46 -0600 Subject: [PATCH 2/3] Add ServiceMethodReturnValueAnalyzer Fixes #14 --- .../OpenStackNetAnalyzers.csproj | 1 + .../ServiceMethodReturnValueAnalyzer.cs | 60 +++++++++++++++++++ .../TypeSymbolExtensions.cs | 18 ++++++ 3 files changed, 79 insertions(+) create mode 100644 OpenStackNetAnalyzers/OpenStackNetAnalyzers/ServiceMethodReturnValueAnalyzer.cs diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj index 4f5adcd..02d5bfc 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj @@ -50,6 +50,7 @@ + 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..235278e 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs @@ -7,6 +7,8 @@ internal static class TypeSymbolExtensions { private const string FullyQualifiedImmutableArrayT = "global::System.Collections.Immutable.ImmutableArray"; + private const string FullyQualifiedTask = "global::System.Threading.Tasks.Task"; + public static bool IsNonNullableValueType(this ITypeSymbol type) { if (type == null) @@ -47,5 +49,21 @@ public static bool IsImmutableArray(this ITypeSymbol type) return string.Equals(FullyQualifiedImmutableArrayT, originalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal); } + + public static bool IsTask(this INamedTypeSymbol symbol) + { + while (symbol != null && symbol.SpecialType != SpecialType.System_Object) + { + if (!symbol.IsGenericType) + { + if (string.Equals(FullyQualifiedTask, symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal)) + return true; + } + + symbol = symbol.BaseType; + } + + return false; + } } } From 167b57fd366104872e71c8c1a68a27b61d27db87 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 6 Feb 2015 11:53:17 -0600 Subject: [PATCH 3/3] Add CancellationTokenAnalyzer This analyzer ensures that asynchronous methods (whose name ends with Async and return a Task) include a CancellationToken parameter. Fixes #22 --- .../CancellationTokenAnalyzer.cs | 55 +++++++++++++++++++ .../OpenStackNetAnalyzers.csproj | 1 + .../TypeSymbolExtensions.cs | 25 +++++++-- 3 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 OpenStackNetAnalyzers/OpenStackNetAnalyzers/CancellationTokenAnalyzer.cs 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 02d5bfc..5463e40 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj @@ -33,6 +33,7 @@ + diff --git a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs index 235278e..453d6a5 100644 --- a/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs +++ b/OpenStackNetAnalyzers/OpenStackNetAnalyzers/TypeSymbolExtensions.cs @@ -9,6 +9,8 @@ internal static class TypeSymbolExtensions 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) @@ -50,20 +52,33 @@ public static bool IsImmutableArray(this ITypeSymbol type) return string.Equals(FullyQualifiedImmutableArrayT, originalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal); } - public static bool IsTask(this INamedTypeSymbol symbol) + public static bool IsTask(this ITypeSymbol symbol) { - while (symbol != null && symbol.SpecialType != SpecialType.System_Object) + INamedTypeSymbol namedSymbol = symbol as INamedTypeSymbol; + while (namedSymbol != null && namedSymbol.SpecialType != SpecialType.System_Object) { - if (!symbol.IsGenericType) + if (!namedSymbol.IsGenericType) { - if (string.Equals(FullyQualifiedTask, symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal)) + if (string.Equals(FullyQualifiedTask, namedSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal)) return true; } - symbol = symbol.BaseType; + 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); + } } }