From c3deaa89d0c1161f6baba04f98fb811bf91a61a3 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 12 Nov 2024 18:15:42 -0800 Subject: [PATCH 01/11] Add new check for ReturnTypes --- .../ClientMethodsAnalyzer.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index 8f51a40f6af..a5cb32efb0f 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -279,6 +279,22 @@ private static bool IsClientMethodReturnType(ISymbolAnalysisContext context, IMe return false; } + private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol syncReturnType) + { + if (asyncReturnType == null || syncReturnType == null) + { + return false; + } + + if (asyncReturnType is INamedTypeSymbol asyncNamedType && asyncNamedType.IsGenericType && asyncNamedType.Name == TaskTypeName) + { + var asyncInnerType = asyncNamedType.TypeArguments.Single(); + return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); + } + + return false; + } + public override void AnalyzeCore(ISymbolAnalysisContext context) { INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; @@ -304,6 +320,11 @@ public override void AnalyzeCore(ISymbolAnalysisContext context) } else { + if (!DoReturnTypesMatch(methodSymbol.ReturnType, syncMember.ReturnType)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); + } + CheckClientMethod(context, syncMember); } } From bbfbade8d20d09f04fe44609b977add984b36f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Wed, 4 Dec 2024 14:58:51 -0800 Subject: [PATCH 02/11] Check for parameters too --- .../ClientMethodsAnalyzer.cs | 659 +++++++++--------- 1 file changed, 333 insertions(+), 326 deletions(-) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index a5cb32efb0f..b59019753cd 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -1,106 +1,106 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace Azure.ClientSdk.Analyzers -{ - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class ClientMethodsAnalyzer : ClientAnalyzerBase - { - private const string AsyncSuffix = "Async"; - - private const string AzureNamespace = "Azure"; - private const string SystemNamespace = "System"; - private const string PageableTypeName = "Pageable"; - private const string AsyncPageableTypeName = "AsyncPageable"; - private const string BinaryDataTypeName = "BinaryData"; - private const string ResponseTypeName = "Response"; - private const string NullableResponseTypeName = "NullableResponse"; - private const string OperationTypeName = "Operation"; - private const string TaskTypeName = "Task"; - private const string BooleanTypeName = "Boolean"; - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(new[] - { - Descriptors.AZC0002, - Descriptors.AZC0003, - Descriptors.AZC0004, - Descriptors.AZC0015, - Descriptors.AZC0017, - Descriptors.AZC0018, - Descriptors.AZC0019, - }); - - private static bool IsRequestContent(IParameterSymbol parameterSymbol) - { - return parameterSymbol.Type.Name == "RequestContent"; - } - - private static bool IsRequestContext(IParameterSymbol parameterSymbol) - { - return parameterSymbol.Name == "context" && parameterSymbol.Type.Name == "RequestContext"; - } - - private static bool IsCancellationToken(IParameterSymbol parameterSymbol) - { - return parameterSymbol.Name == "cancellationToken" && parameterSymbol.Type.Name == "CancellationToken"; - } - - private static bool IsCancellationOrRequestContext(IParameterSymbol parameterSymbol) - { - return IsCancellationToken(parameterSymbol) || IsRequestContext(parameterSymbol); - } - - private static void CheckServiceMethod(ISymbolAnalysisContext context, IMethodSymbol member) - { - var lastArgument = member.Parameters.LastOrDefault(); - var isLastArgumentCancellationOrRequestContext = lastArgument != null && IsCancellationOrRequestContext(lastArgument); - - if (!isLastArgumentCancellationOrRequestContext) - { - var overloadSupportsCancellations = FindMethod( - member.ContainingType.GetMembers(member.Name).OfType(), - member.TypeParameters, - member.Parameters, - p => IsCancellationToken(p)); - - if (overloadSupportsCancellations == null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); - } - } - else if (IsCancellationToken(lastArgument)) - { - if (!lastArgument.IsOptional) - { - var overloadWithCancellationToken = FindMethod( - member.ContainingType.GetMembers(member.Name).OfType(), - member.TypeParameters, - member.Parameters.RemoveAt(member.Parameters.Length - 1)); - - if (overloadWithCancellationToken == null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); - } - } - - if (member.Parameters.FirstOrDefault(IsRequestContent) != null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0017, member.Locations.FirstOrDefault()), member); - } - } - else if (IsRequestContext(lastArgument)) - { - CheckProtocolMethodReturnType(context, member); - CheckProtocolMethodParameters(context, member); - } - } - +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Azure.ClientSdk.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ClientMethodsAnalyzer : ClientAnalyzerBase + { + private const string AsyncSuffix = "Async"; + + private const string AzureNamespace = "Azure"; + private const string SystemNamespace = "System"; + private const string PageableTypeName = "Pageable"; + private const string AsyncPageableTypeName = "AsyncPageable"; + private const string BinaryDataTypeName = "BinaryData"; + private const string ResponseTypeName = "Response"; + private const string NullableResponseTypeName = "NullableResponse"; + private const string OperationTypeName = "Operation"; + private const string TaskTypeName = "Task"; + private const string BooleanTypeName = "Boolean"; + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(new[] + { + Descriptors.AZC0002, + Descriptors.AZC0003, + Descriptors.AZC0004, + Descriptors.AZC0015, + Descriptors.AZC0017, + Descriptors.AZC0018, + Descriptors.AZC0019, + }); + + private static bool IsRequestContent(IParameterSymbol parameterSymbol) + { + return parameterSymbol.Type.Name == "RequestContent"; + } + + private static bool IsRequestContext(IParameterSymbol parameterSymbol) + { + return parameterSymbol.Name == "context" && parameterSymbol.Type.Name == "RequestContext"; + } + + private static bool IsCancellationToken(IParameterSymbol parameterSymbol) + { + return parameterSymbol.Name == "cancellationToken" && parameterSymbol.Type.Name == "CancellationToken"; + } + + private static bool IsCancellationOrRequestContext(IParameterSymbol parameterSymbol) + { + return IsCancellationToken(parameterSymbol) || IsRequestContext(parameterSymbol); + } + + private static void CheckServiceMethod(ISymbolAnalysisContext context, IMethodSymbol member) + { + var lastArgument = member.Parameters.LastOrDefault(); + var isLastArgumentCancellationOrRequestContext = lastArgument != null && IsCancellationOrRequestContext(lastArgument); + + if (!isLastArgumentCancellationOrRequestContext) + { + var overloadSupportsCancellations = FindMethod( + member.ContainingType.GetMembers(member.Name).OfType(), + member.TypeParameters, + member.Parameters, + p => IsCancellationToken(p)); + + if (overloadSupportsCancellations == null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); + } + } + else if (IsCancellationToken(lastArgument)) + { + if (!lastArgument.IsOptional) + { + var overloadWithCancellationToken = FindMethod( + member.ContainingType.GetMembers(member.Name).OfType(), + member.TypeParameters, + member.Parameters.RemoveAt(member.Parameters.Length - 1)); + + if (overloadWithCancellationToken == null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); + } + } + + if (member.Parameters.FirstOrDefault(IsRequestContent) != null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0017, member.Locations.FirstOrDefault()), member); + } + } + else if (IsRequestContext(lastArgument)) + { + CheckProtocolMethodReturnType(context, member); + CheckProtocolMethodParameters(context, member); + } + } + private static string GetFullNamespaceName(IParameterSymbol parameter) { var currentNamespace = parameter.Type.ContainingNamespace; @@ -113,227 +113,234 @@ private static string GetFullNamespaceName(IParameterSymbol parameter) currentName = currentNamespace.Name; } return fullNamespace; - } - - // A protocol method should not have model as parameter. If it has ambiguity with convenience method, it should have required RequestContext. - // Ambiguity: doesn't have a RequestContent, but there is a method ending with CancellationToken has same type of parameters - // No ambiguity: has RequestContent. - private static void CheckProtocolMethodParameters(ISymbolAnalysisContext context, IMethodSymbol method) - { - var containsModel = method.Parameters.Any(p => - { - var fullNamespace = GetFullNamespaceName(p); - return !fullNamespace.StartsWith("System") && !fullNamespace.StartsWith("Azure"); - }); - - if (containsModel) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - return; - } - - var requestContent = method.Parameters.FirstOrDefault(IsRequestContent); - if (requestContent == null && method.Parameters.Last().IsOptional) - { - INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; - IEnumerable methodList = type.GetMembers(method.Name).OfType().Where(member => !SymbolEqualityComparer.Default.Equals(member, method)); - ImmutableArray parametersWithoutLast = method.Parameters.RemoveAt(method.Parameters.Length - 1); - IMethodSymbol convenienceMethod = FindMethod(methodList, method.TypeParameters, parametersWithoutLast, symbol => IsCancellationToken(symbol)); - if (convenienceMethod != null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0019, method.Locations.FirstOrDefault()), method); - } - } - } - - // A protocol method should not have model as type. Accepted return type: Response, Task, Response, Task>, Pageable, AsyncPageable, Operation, Task>, Operation, Task, Operation>, Task>> - private static void CheckProtocolMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method) - { - bool IsValidPageable(ITypeSymbol typeSymbol) - { - var pageableTypeSymbol = typeSymbol as INamedTypeSymbol; - if (!pageableTypeSymbol.IsGenericType) - { - return false; - } - - var pageableReturn = pageableTypeSymbol.TypeArguments.Single(); - if (!IsOrImplements(pageableReturn, BinaryDataTypeName, SystemNamespace)) - { - return false; - } - - return true; - } - - ITypeSymbol originalType = method.ReturnType; - ITypeSymbol unwrappedType = method.ReturnType; - - if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && - namedTypeSymbol.IsGenericType && - namedTypeSymbol.Name == TaskTypeName) - { - unwrappedType = namedTypeSymbol.TypeArguments.Single(); - } - - if (IsOrImplements(unwrappedType, ResponseTypeName, AzureNamespace)) - { - if (unwrappedType is INamedTypeSymbol responseTypeSymbol && responseTypeSymbol.IsGenericType) - { - var responseReturn = responseTypeSymbol.TypeArguments.Single(); - if (responseReturn.Name != BooleanTypeName) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - } - return; - } - else if (IsOrImplements(unwrappedType, OperationTypeName, AzureNamespace)) - { - if (unwrappedType is INamedTypeSymbol operationTypeSymbol && operationTypeSymbol.IsGenericType) - { - var operationReturn = operationTypeSymbol.TypeArguments.Single(); - if (IsOrImplements(operationReturn, PageableTypeName, AzureNamespace) || IsOrImplements(operationReturn, AsyncPageableTypeName, AzureNamespace)) - { - if (!IsValidPageable(operationReturn)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - return; - } - - if (!IsOrImplements(operationReturn, BinaryDataTypeName, SystemNamespace)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - } - return; - } - else if (IsOrImplements(originalType, PageableTypeName, AzureNamespace) || IsOrImplements(originalType, AsyncPageableTypeName, AzureNamespace)) - { - if (!IsValidPageable(originalType)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - return; - } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - - private static void CheckClientMethod(ISymbolAnalysisContext context, IMethodSymbol member) - { - CheckClientMethodReturnType(context, member); - - if (!member.IsVirtual && !member.IsOverride) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0003, member.Locations.First()), member); - } - } - - private static bool IsOrImplements(ITypeSymbol typeSymbol, string typeName, string namespaceName) - { - if (typeSymbol.Name == typeName && typeSymbol.ContainingNamespace.Name == namespaceName && typeSymbol.ContainingNamespace.ContainingNamespace.Name == "") - { - return true; - } - - if (typeSymbol.BaseType != null) - { - return IsOrImplements(typeSymbol.BaseType, typeName, namespaceName); - } - - return false; - } - - private static void CheckClientMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method) - { - IsClientMethodReturnType(context, method, true); - } - - private static bool IsClientMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method, bool throwError = false) - { - ITypeSymbol originalType = method.ReturnType; - ITypeSymbol unwrappedType = method.ReturnType; - - if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && - namedTypeSymbol.IsGenericType && - namedTypeSymbol.Name == TaskTypeName) - { - unwrappedType = namedTypeSymbol.TypeArguments.Single(); - } - - if (IsOrImplements(unwrappedType, ResponseTypeName, AzureNamespace) || - IsOrImplements(unwrappedType, NullableResponseTypeName, AzureNamespace) || - IsOrImplements(unwrappedType, OperationTypeName, AzureNamespace) || - IsOrImplements(originalType, PageableTypeName, AzureNamespace) || - IsOrImplements(originalType, AsyncPageableTypeName, AzureNamespace)) - { - return true; - } - - if (throwError) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0015, method.Locations.FirstOrDefault(), originalType.ToDisplayString()), method); - } - return false; - } - - private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol syncReturnType) - { - if (asyncReturnType == null || syncReturnType == null) - { - return false; - } - - if (asyncReturnType is INamedTypeSymbol asyncNamedType && asyncNamedType.IsGenericType && asyncNamedType.Name == TaskTypeName) - { - var asyncInnerType = asyncNamedType.TypeArguments.Single(); - return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); - } - - return false; - } - - public override void AnalyzeCore(ISymbolAnalysisContext context) - { - INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; - foreach (var member in type.GetMembers()) - { - var methodSymbol = member as IMethodSymbol; - if (methodSymbol == null || methodSymbol.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - if (methodSymbol.Name.EndsWith(AsyncSuffix)) - { - CheckClientMethod(context, methodSymbol); - - var syncMemberName = member.Name.Substring(0, member.Name.Length - AsyncSuffix.Length); - - var syncMember = FindMethod(type.GetMembers(syncMemberName).OfType(), methodSymbol.TypeParameters, methodSymbol.Parameters); - - if (syncMember == null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); - } - else - { - if (!DoReturnTypesMatch(methodSymbol.ReturnType, syncMember.ReturnType)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); - } - - CheckClientMethod(context, syncMember); - } - } - - if (IsClientMethodReturnType(context, methodSymbol, false)) - { - CheckServiceMethod(context, methodSymbol); - } - } - } - } -} + } + + // A protocol method should not have model as parameter. If it has ambiguity with convenience method, it should have required RequestContext. + // Ambiguity: doesn't have a RequestContent, but there is a method ending with CancellationToken has same type of parameters + // No ambiguity: has RequestContent. + private static void CheckProtocolMethodParameters(ISymbolAnalysisContext context, IMethodSymbol method) + { + var containsModel = method.Parameters.Any(p => + { + var fullNamespace = GetFullNamespaceName(p); + return !fullNamespace.StartsWith("System") && !fullNamespace.StartsWith("Azure"); + }); + + if (containsModel) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + return; + } + + var requestContent = method.Parameters.FirstOrDefault(IsRequestContent); + if (requestContent == null && method.Parameters.Last().IsOptional) + { + INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; + IEnumerable methodList = type.GetMembers(method.Name).OfType().Where(member => !SymbolEqualityComparer.Default.Equals(member, method)); + ImmutableArray parametersWithoutLast = method.Parameters.RemoveAt(method.Parameters.Length - 1); + IMethodSymbol convenienceMethod = FindMethod(methodList, method.TypeParameters, parametersWithoutLast, symbol => IsCancellationToken(symbol)); + if (convenienceMethod != null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0019, method.Locations.FirstOrDefault()), method); + } + } + } + + // A protocol method should not have model as type. Accepted return type: Response, Task, Response, Task>, Pageable, AsyncPageable, Operation, Task>, Operation, Task, Operation>, Task>> + private static void CheckProtocolMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method) + { + bool IsValidPageable(ITypeSymbol typeSymbol) + { + var pageableTypeSymbol = typeSymbol as INamedTypeSymbol; + if (!pageableTypeSymbol.IsGenericType) + { + return false; + } + + var pageableReturn = pageableTypeSymbol.TypeArguments.Single(); + if (!IsOrImplements(pageableReturn, BinaryDataTypeName, SystemNamespace)) + { + return false; + } + + return true; + } + + ITypeSymbol originalType = method.ReturnType; + ITypeSymbol unwrappedType = method.ReturnType; + + if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && + namedTypeSymbol.IsGenericType && + namedTypeSymbol.Name == TaskTypeName) + { + unwrappedType = namedTypeSymbol.TypeArguments.Single(); + } + + if (IsOrImplements(unwrappedType, ResponseTypeName, AzureNamespace)) + { + if (unwrappedType is INamedTypeSymbol responseTypeSymbol && responseTypeSymbol.IsGenericType) + { + var responseReturn = responseTypeSymbol.TypeArguments.Single(); + if (responseReturn.Name != BooleanTypeName) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + } + return; + } + else if (IsOrImplements(unwrappedType, OperationTypeName, AzureNamespace)) + { + if (unwrappedType is INamedTypeSymbol operationTypeSymbol && operationTypeSymbol.IsGenericType) + { + var operationReturn = operationTypeSymbol.TypeArguments.Single(); + if (IsOrImplements(operationReturn, PageableTypeName, AzureNamespace) || IsOrImplements(operationReturn, AsyncPageableTypeName, AzureNamespace)) + { + if (!IsValidPageable(operationReturn)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + return; + } + + if (!IsOrImplements(operationReturn, BinaryDataTypeName, SystemNamespace)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + } + return; + } + else if (IsOrImplements(originalType, PageableTypeName, AzureNamespace) || IsOrImplements(originalType, AsyncPageableTypeName, AzureNamespace)) + { + if (!IsValidPageable(originalType)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + return; + } + + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + + private static void CheckClientMethod(ISymbolAnalysisContext context, IMethodSymbol member) + { + CheckClientMethodReturnType(context, member); + + if (!member.IsVirtual && !member.IsOverride) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0003, member.Locations.First()), member); + } + } + + private static bool IsOrImplements(ITypeSymbol typeSymbol, string typeName, string namespaceName) + { + if (typeSymbol.Name == typeName && typeSymbol.ContainingNamespace.Name == namespaceName && typeSymbol.ContainingNamespace.ContainingNamespace.Name == "") + { + return true; + } + + if (typeSymbol.BaseType != null) + { + return IsOrImplements(typeSymbol.BaseType, typeName, namespaceName); + } + + return false; + } + + private static void CheckClientMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method) + { + IsClientMethodReturnType(context, method, true); + } + + private static bool IsClientMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method, bool throwError = false) + { + ITypeSymbol originalType = method.ReturnType; + ITypeSymbol unwrappedType = method.ReturnType; + + if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && + namedTypeSymbol.IsGenericType && + namedTypeSymbol.Name == TaskTypeName) + { + unwrappedType = namedTypeSymbol.TypeArguments.Single(); + } + + if (IsOrImplements(unwrappedType, ResponseTypeName, AzureNamespace) || + IsOrImplements(unwrappedType, NullableResponseTypeName, AzureNamespace) || + IsOrImplements(unwrappedType, OperationTypeName, AzureNamespace) || + IsOrImplements(originalType, PageableTypeName, AzureNamespace) || + IsOrImplements(originalType, AsyncPageableTypeName, AzureNamespace)) + { + return true; + } + + if (throwError) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0015, method.Locations.FirstOrDefault(), originalType.ToDisplayString()), method); + } + return false; + } + + private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol syncReturnType) + { + if (asyncReturnType == null || syncReturnType == null) + { + return false; + } + + if (asyncReturnType is INamedTypeSymbol asyncNamedType && asyncNamedType.IsGenericType && asyncNamedType.Name == TaskTypeName) + { + var asyncInnerType = asyncNamedType.TypeArguments.Single(); + return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); + } + + return false; + } + + public override void AnalyzeCore(ISymbolAnalysisContext context) + { + INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; + foreach (var member in type.GetMembers()) + { + var methodSymbol = member as IMethodSymbol; + if (methodSymbol == null || methodSymbol.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + if (methodSymbol.Name.EndsWith(AsyncSuffix)) + { + CheckClientMethod(context, methodSymbol); + + var syncMemberName = member.Name.Substring(0, member.Name.Length - AsyncSuffix.Length); + + var syncMember = FindMethod( + type.GetMembers(syncMemberName).OfType(), + methodSymbol.TypeParameters, + methodSymbol.Parameters); + + if (syncMember == null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); + } + else + { + if (!DoReturnTypesMatch(methodSymbol.ReturnType, syncMember.ReturnType)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); + } + + if (!methodSymbol.Parameters.SequenceEqual(syncMember.Parameters, ParameterEquivalenceComparer.Default)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0005, member.Locations.First()), member); + } + CheckClientMethod(context, syncMember); + } + } + + if (IsClientMethodReturnType(context, methodSymbol, false)) + { + CheckServiceMethod(context, methodSymbol); + } + } + } + } +} From 6168a70baba4a5ff782728bce297c1439721d20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Wed, 4 Dec 2024 15:02:02 -0800 Subject: [PATCH 03/11] Revert "Check for parameters too" This reverts commit bbfbade8d20d09f04fe44609b977add984b36f73. --- .../ClientMethodsAnalyzer.cs | 659 +++++++++--------- 1 file changed, 326 insertions(+), 333 deletions(-) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index b59019753cd..a5cb32efb0f 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -1,106 +1,106 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace Azure.ClientSdk.Analyzers -{ - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class ClientMethodsAnalyzer : ClientAnalyzerBase - { - private const string AsyncSuffix = "Async"; - - private const string AzureNamespace = "Azure"; - private const string SystemNamespace = "System"; - private const string PageableTypeName = "Pageable"; - private const string AsyncPageableTypeName = "AsyncPageable"; - private const string BinaryDataTypeName = "BinaryData"; - private const string ResponseTypeName = "Response"; - private const string NullableResponseTypeName = "NullableResponse"; - private const string OperationTypeName = "Operation"; - private const string TaskTypeName = "Task"; - private const string BooleanTypeName = "Boolean"; - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(new[] - { - Descriptors.AZC0002, - Descriptors.AZC0003, - Descriptors.AZC0004, - Descriptors.AZC0015, - Descriptors.AZC0017, - Descriptors.AZC0018, - Descriptors.AZC0019, - }); - - private static bool IsRequestContent(IParameterSymbol parameterSymbol) - { - return parameterSymbol.Type.Name == "RequestContent"; - } - - private static bool IsRequestContext(IParameterSymbol parameterSymbol) - { - return parameterSymbol.Name == "context" && parameterSymbol.Type.Name == "RequestContext"; - } - - private static bool IsCancellationToken(IParameterSymbol parameterSymbol) - { - return parameterSymbol.Name == "cancellationToken" && parameterSymbol.Type.Name == "CancellationToken"; - } - - private static bool IsCancellationOrRequestContext(IParameterSymbol parameterSymbol) - { - return IsCancellationToken(parameterSymbol) || IsRequestContext(parameterSymbol); - } - - private static void CheckServiceMethod(ISymbolAnalysisContext context, IMethodSymbol member) - { - var lastArgument = member.Parameters.LastOrDefault(); - var isLastArgumentCancellationOrRequestContext = lastArgument != null && IsCancellationOrRequestContext(lastArgument); - - if (!isLastArgumentCancellationOrRequestContext) - { - var overloadSupportsCancellations = FindMethod( - member.ContainingType.GetMembers(member.Name).OfType(), - member.TypeParameters, - member.Parameters, - p => IsCancellationToken(p)); - - if (overloadSupportsCancellations == null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); - } - } - else if (IsCancellationToken(lastArgument)) - { - if (!lastArgument.IsOptional) - { - var overloadWithCancellationToken = FindMethod( - member.ContainingType.GetMembers(member.Name).OfType(), - member.TypeParameters, - member.Parameters.RemoveAt(member.Parameters.Length - 1)); - - if (overloadWithCancellationToken == null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); - } - } - - if (member.Parameters.FirstOrDefault(IsRequestContent) != null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0017, member.Locations.FirstOrDefault()), member); - } - } - else if (IsRequestContext(lastArgument)) - { - CheckProtocolMethodReturnType(context, member); - CheckProtocolMethodParameters(context, member); - } - } - +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Azure.ClientSdk.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ClientMethodsAnalyzer : ClientAnalyzerBase + { + private const string AsyncSuffix = "Async"; + + private const string AzureNamespace = "Azure"; + private const string SystemNamespace = "System"; + private const string PageableTypeName = "Pageable"; + private const string AsyncPageableTypeName = "AsyncPageable"; + private const string BinaryDataTypeName = "BinaryData"; + private const string ResponseTypeName = "Response"; + private const string NullableResponseTypeName = "NullableResponse"; + private const string OperationTypeName = "Operation"; + private const string TaskTypeName = "Task"; + private const string BooleanTypeName = "Boolean"; + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(new[] + { + Descriptors.AZC0002, + Descriptors.AZC0003, + Descriptors.AZC0004, + Descriptors.AZC0015, + Descriptors.AZC0017, + Descriptors.AZC0018, + Descriptors.AZC0019, + }); + + private static bool IsRequestContent(IParameterSymbol parameterSymbol) + { + return parameterSymbol.Type.Name == "RequestContent"; + } + + private static bool IsRequestContext(IParameterSymbol parameterSymbol) + { + return parameterSymbol.Name == "context" && parameterSymbol.Type.Name == "RequestContext"; + } + + private static bool IsCancellationToken(IParameterSymbol parameterSymbol) + { + return parameterSymbol.Name == "cancellationToken" && parameterSymbol.Type.Name == "CancellationToken"; + } + + private static bool IsCancellationOrRequestContext(IParameterSymbol parameterSymbol) + { + return IsCancellationToken(parameterSymbol) || IsRequestContext(parameterSymbol); + } + + private static void CheckServiceMethod(ISymbolAnalysisContext context, IMethodSymbol member) + { + var lastArgument = member.Parameters.LastOrDefault(); + var isLastArgumentCancellationOrRequestContext = lastArgument != null && IsCancellationOrRequestContext(lastArgument); + + if (!isLastArgumentCancellationOrRequestContext) + { + var overloadSupportsCancellations = FindMethod( + member.ContainingType.GetMembers(member.Name).OfType(), + member.TypeParameters, + member.Parameters, + p => IsCancellationToken(p)); + + if (overloadSupportsCancellations == null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); + } + } + else if (IsCancellationToken(lastArgument)) + { + if (!lastArgument.IsOptional) + { + var overloadWithCancellationToken = FindMethod( + member.ContainingType.GetMembers(member.Name).OfType(), + member.TypeParameters, + member.Parameters.RemoveAt(member.Parameters.Length - 1)); + + if (overloadWithCancellationToken == null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); + } + } + + if (member.Parameters.FirstOrDefault(IsRequestContent) != null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0017, member.Locations.FirstOrDefault()), member); + } + } + else if (IsRequestContext(lastArgument)) + { + CheckProtocolMethodReturnType(context, member); + CheckProtocolMethodParameters(context, member); + } + } + private static string GetFullNamespaceName(IParameterSymbol parameter) { var currentNamespace = parameter.Type.ContainingNamespace; @@ -113,234 +113,227 @@ private static string GetFullNamespaceName(IParameterSymbol parameter) currentName = currentNamespace.Name; } return fullNamespace; - } - - // A protocol method should not have model as parameter. If it has ambiguity with convenience method, it should have required RequestContext. - // Ambiguity: doesn't have a RequestContent, but there is a method ending with CancellationToken has same type of parameters - // No ambiguity: has RequestContent. - private static void CheckProtocolMethodParameters(ISymbolAnalysisContext context, IMethodSymbol method) - { - var containsModel = method.Parameters.Any(p => - { - var fullNamespace = GetFullNamespaceName(p); - return !fullNamespace.StartsWith("System") && !fullNamespace.StartsWith("Azure"); - }); - - if (containsModel) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - return; - } - - var requestContent = method.Parameters.FirstOrDefault(IsRequestContent); - if (requestContent == null && method.Parameters.Last().IsOptional) - { - INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; - IEnumerable methodList = type.GetMembers(method.Name).OfType().Where(member => !SymbolEqualityComparer.Default.Equals(member, method)); - ImmutableArray parametersWithoutLast = method.Parameters.RemoveAt(method.Parameters.Length - 1); - IMethodSymbol convenienceMethod = FindMethod(methodList, method.TypeParameters, parametersWithoutLast, symbol => IsCancellationToken(symbol)); - if (convenienceMethod != null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0019, method.Locations.FirstOrDefault()), method); - } - } - } - - // A protocol method should not have model as type. Accepted return type: Response, Task, Response, Task>, Pageable, AsyncPageable, Operation, Task>, Operation, Task, Operation>, Task>> - private static void CheckProtocolMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method) - { - bool IsValidPageable(ITypeSymbol typeSymbol) - { - var pageableTypeSymbol = typeSymbol as INamedTypeSymbol; - if (!pageableTypeSymbol.IsGenericType) - { - return false; - } - - var pageableReturn = pageableTypeSymbol.TypeArguments.Single(); - if (!IsOrImplements(pageableReturn, BinaryDataTypeName, SystemNamespace)) - { - return false; - } - - return true; - } - - ITypeSymbol originalType = method.ReturnType; - ITypeSymbol unwrappedType = method.ReturnType; - - if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && - namedTypeSymbol.IsGenericType && - namedTypeSymbol.Name == TaskTypeName) - { - unwrappedType = namedTypeSymbol.TypeArguments.Single(); - } - - if (IsOrImplements(unwrappedType, ResponseTypeName, AzureNamespace)) - { - if (unwrappedType is INamedTypeSymbol responseTypeSymbol && responseTypeSymbol.IsGenericType) - { - var responseReturn = responseTypeSymbol.TypeArguments.Single(); - if (responseReturn.Name != BooleanTypeName) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - } - return; - } - else if (IsOrImplements(unwrappedType, OperationTypeName, AzureNamespace)) - { - if (unwrappedType is INamedTypeSymbol operationTypeSymbol && operationTypeSymbol.IsGenericType) - { - var operationReturn = operationTypeSymbol.TypeArguments.Single(); - if (IsOrImplements(operationReturn, PageableTypeName, AzureNamespace) || IsOrImplements(operationReturn, AsyncPageableTypeName, AzureNamespace)) - { - if (!IsValidPageable(operationReturn)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - return; - } - - if (!IsOrImplements(operationReturn, BinaryDataTypeName, SystemNamespace)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - } - return; - } - else if (IsOrImplements(originalType, PageableTypeName, AzureNamespace) || IsOrImplements(originalType, AsyncPageableTypeName, AzureNamespace)) - { - if (!IsValidPageable(originalType)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - return; - } - - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); - } - - private static void CheckClientMethod(ISymbolAnalysisContext context, IMethodSymbol member) - { - CheckClientMethodReturnType(context, member); - - if (!member.IsVirtual && !member.IsOverride) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0003, member.Locations.First()), member); - } - } - - private static bool IsOrImplements(ITypeSymbol typeSymbol, string typeName, string namespaceName) - { - if (typeSymbol.Name == typeName && typeSymbol.ContainingNamespace.Name == namespaceName && typeSymbol.ContainingNamespace.ContainingNamespace.Name == "") - { - return true; - } - - if (typeSymbol.BaseType != null) - { - return IsOrImplements(typeSymbol.BaseType, typeName, namespaceName); - } - - return false; - } - - private static void CheckClientMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method) - { - IsClientMethodReturnType(context, method, true); - } - - private static bool IsClientMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method, bool throwError = false) - { - ITypeSymbol originalType = method.ReturnType; - ITypeSymbol unwrappedType = method.ReturnType; - - if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && - namedTypeSymbol.IsGenericType && - namedTypeSymbol.Name == TaskTypeName) - { - unwrappedType = namedTypeSymbol.TypeArguments.Single(); - } - - if (IsOrImplements(unwrappedType, ResponseTypeName, AzureNamespace) || - IsOrImplements(unwrappedType, NullableResponseTypeName, AzureNamespace) || - IsOrImplements(unwrappedType, OperationTypeName, AzureNamespace) || - IsOrImplements(originalType, PageableTypeName, AzureNamespace) || - IsOrImplements(originalType, AsyncPageableTypeName, AzureNamespace)) - { - return true; - } - - if (throwError) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0015, method.Locations.FirstOrDefault(), originalType.ToDisplayString()), method); - } - return false; - } - - private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol syncReturnType) - { - if (asyncReturnType == null || syncReturnType == null) - { - return false; - } - - if (asyncReturnType is INamedTypeSymbol asyncNamedType && asyncNamedType.IsGenericType && asyncNamedType.Name == TaskTypeName) - { - var asyncInnerType = asyncNamedType.TypeArguments.Single(); - return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); - } - - return false; - } - - public override void AnalyzeCore(ISymbolAnalysisContext context) - { - INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; - foreach (var member in type.GetMembers()) - { - var methodSymbol = member as IMethodSymbol; - if (methodSymbol == null || methodSymbol.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - if (methodSymbol.Name.EndsWith(AsyncSuffix)) - { - CheckClientMethod(context, methodSymbol); - - var syncMemberName = member.Name.Substring(0, member.Name.Length - AsyncSuffix.Length); - - var syncMember = FindMethod( - type.GetMembers(syncMemberName).OfType(), - methodSymbol.TypeParameters, - methodSymbol.Parameters); - - if (syncMember == null) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); - } - else - { - if (!DoReturnTypesMatch(methodSymbol.ReturnType, syncMember.ReturnType)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); - } - - if (!methodSymbol.Parameters.SequenceEqual(syncMember.Parameters, ParameterEquivalenceComparer.Default)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0005, member.Locations.First()), member); - } - CheckClientMethod(context, syncMember); - } - } - - if (IsClientMethodReturnType(context, methodSymbol, false)) - { - CheckServiceMethod(context, methodSymbol); - } - } - } - } -} + } + + // A protocol method should not have model as parameter. If it has ambiguity with convenience method, it should have required RequestContext. + // Ambiguity: doesn't have a RequestContent, but there is a method ending with CancellationToken has same type of parameters + // No ambiguity: has RequestContent. + private static void CheckProtocolMethodParameters(ISymbolAnalysisContext context, IMethodSymbol method) + { + var containsModel = method.Parameters.Any(p => + { + var fullNamespace = GetFullNamespaceName(p); + return !fullNamespace.StartsWith("System") && !fullNamespace.StartsWith("Azure"); + }); + + if (containsModel) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + return; + } + + var requestContent = method.Parameters.FirstOrDefault(IsRequestContent); + if (requestContent == null && method.Parameters.Last().IsOptional) + { + INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; + IEnumerable methodList = type.GetMembers(method.Name).OfType().Where(member => !SymbolEqualityComparer.Default.Equals(member, method)); + ImmutableArray parametersWithoutLast = method.Parameters.RemoveAt(method.Parameters.Length - 1); + IMethodSymbol convenienceMethod = FindMethod(methodList, method.TypeParameters, parametersWithoutLast, symbol => IsCancellationToken(symbol)); + if (convenienceMethod != null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0019, method.Locations.FirstOrDefault()), method); + } + } + } + + // A protocol method should not have model as type. Accepted return type: Response, Task, Response, Task>, Pageable, AsyncPageable, Operation, Task>, Operation, Task, Operation>, Task>> + private static void CheckProtocolMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method) + { + bool IsValidPageable(ITypeSymbol typeSymbol) + { + var pageableTypeSymbol = typeSymbol as INamedTypeSymbol; + if (!pageableTypeSymbol.IsGenericType) + { + return false; + } + + var pageableReturn = pageableTypeSymbol.TypeArguments.Single(); + if (!IsOrImplements(pageableReturn, BinaryDataTypeName, SystemNamespace)) + { + return false; + } + + return true; + } + + ITypeSymbol originalType = method.ReturnType; + ITypeSymbol unwrappedType = method.ReturnType; + + if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && + namedTypeSymbol.IsGenericType && + namedTypeSymbol.Name == TaskTypeName) + { + unwrappedType = namedTypeSymbol.TypeArguments.Single(); + } + + if (IsOrImplements(unwrappedType, ResponseTypeName, AzureNamespace)) + { + if (unwrappedType is INamedTypeSymbol responseTypeSymbol && responseTypeSymbol.IsGenericType) + { + var responseReturn = responseTypeSymbol.TypeArguments.Single(); + if (responseReturn.Name != BooleanTypeName) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + } + return; + } + else if (IsOrImplements(unwrappedType, OperationTypeName, AzureNamespace)) + { + if (unwrappedType is INamedTypeSymbol operationTypeSymbol && operationTypeSymbol.IsGenericType) + { + var operationReturn = operationTypeSymbol.TypeArguments.Single(); + if (IsOrImplements(operationReturn, PageableTypeName, AzureNamespace) || IsOrImplements(operationReturn, AsyncPageableTypeName, AzureNamespace)) + { + if (!IsValidPageable(operationReturn)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + return; + } + + if (!IsOrImplements(operationReturn, BinaryDataTypeName, SystemNamespace)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + } + return; + } + else if (IsOrImplements(originalType, PageableTypeName, AzureNamespace) || IsOrImplements(originalType, AsyncPageableTypeName, AzureNamespace)) + { + if (!IsValidPageable(originalType)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + return; + } + + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + + private static void CheckClientMethod(ISymbolAnalysisContext context, IMethodSymbol member) + { + CheckClientMethodReturnType(context, member); + + if (!member.IsVirtual && !member.IsOverride) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0003, member.Locations.First()), member); + } + } + + private static bool IsOrImplements(ITypeSymbol typeSymbol, string typeName, string namespaceName) + { + if (typeSymbol.Name == typeName && typeSymbol.ContainingNamespace.Name == namespaceName && typeSymbol.ContainingNamespace.ContainingNamespace.Name == "") + { + return true; + } + + if (typeSymbol.BaseType != null) + { + return IsOrImplements(typeSymbol.BaseType, typeName, namespaceName); + } + + return false; + } + + private static void CheckClientMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method) + { + IsClientMethodReturnType(context, method, true); + } + + private static bool IsClientMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method, bool throwError = false) + { + ITypeSymbol originalType = method.ReturnType; + ITypeSymbol unwrappedType = method.ReturnType; + + if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && + namedTypeSymbol.IsGenericType && + namedTypeSymbol.Name == TaskTypeName) + { + unwrappedType = namedTypeSymbol.TypeArguments.Single(); + } + + if (IsOrImplements(unwrappedType, ResponseTypeName, AzureNamespace) || + IsOrImplements(unwrappedType, NullableResponseTypeName, AzureNamespace) || + IsOrImplements(unwrappedType, OperationTypeName, AzureNamespace) || + IsOrImplements(originalType, PageableTypeName, AzureNamespace) || + IsOrImplements(originalType, AsyncPageableTypeName, AzureNamespace)) + { + return true; + } + + if (throwError) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0015, method.Locations.FirstOrDefault(), originalType.ToDisplayString()), method); + } + return false; + } + + private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol syncReturnType) + { + if (asyncReturnType == null || syncReturnType == null) + { + return false; + } + + if (asyncReturnType is INamedTypeSymbol asyncNamedType && asyncNamedType.IsGenericType && asyncNamedType.Name == TaskTypeName) + { + var asyncInnerType = asyncNamedType.TypeArguments.Single(); + return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); + } + + return false; + } + + public override void AnalyzeCore(ISymbolAnalysisContext context) + { + INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; + foreach (var member in type.GetMembers()) + { + var methodSymbol = member as IMethodSymbol; + if (methodSymbol == null || methodSymbol.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + if (methodSymbol.Name.EndsWith(AsyncSuffix)) + { + CheckClientMethod(context, methodSymbol); + + var syncMemberName = member.Name.Substring(0, member.Name.Length - AsyncSuffix.Length); + + var syncMember = FindMethod(type.GetMembers(syncMemberName).OfType(), methodSymbol.TypeParameters, methodSymbol.Parameters); + + if (syncMember == null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); + } + else + { + if (!DoReturnTypesMatch(methodSymbol.ReturnType, syncMember.ReturnType)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); + } + + CheckClientMethod(context, syncMember); + } + } + + if (IsClientMethodReturnType(context, methodSymbol, false)) + { + CheckServiceMethod(context, methodSymbol); + } + } + } + } +} From 50cf3ee82c83ed88f06b107680225fd2adb16736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Wed, 4 Dec 2024 15:03:42 -0800 Subject: [PATCH 04/11] Check for parameters in both methods --- .../Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index a5cb32efb0f..9186cb72806 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -325,6 +325,11 @@ public override void AnalyzeCore(ISymbolAnalysisContext context) context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); } + if (!methodSymbol.Parameters.SequenceEqual(syncMember.Parameters, ParameterEquivalenceComparer.Default)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0005, member.Locations.First()), member); + } + CheckClientMethod(context, syncMember); } } From 0b4834b992f110d0f2bedc5f9e0d5b801fa042db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Mon, 9 Dec 2024 11:34:36 -0800 Subject: [PATCH 05/11] Skip check for AMQP libraries --- .../ClientMethodsAnalyzer.cs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index 9186cb72806..23c6a91a01e 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -295,9 +295,31 @@ private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol return false; } + private bool IsAmqpBasedLibrary(INamedTypeSymbol type) + { + var namespaceName = type.ContainingNamespace.ToDisplayString(); + + // List of AMQP-based libraries that are exempted from this rule. + if (namespaceName.StartsWith("Azure.Messaging.EventHubs") || + namespaceName.StartsWith("Azure.Messaging.ServiceBus") || + namespaceName.StartsWith("Azure.Messaging.WebPubSub")) + { + return true; + } + + return false; + } + public override void AnalyzeCore(ISymbolAnalysisContext context) { INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; + + // Skip the verification for AMQP-based SDKs + if (IsAmqpBasedLibrary(type)) + { + return; + } + foreach (var member in type.GetMembers()) { var methodSymbol = member as IMethodSymbol; @@ -325,9 +347,9 @@ public override void AnalyzeCore(ISymbolAnalysisContext context) context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); } - if (!methodSymbol.Parameters.SequenceEqual(syncMember.Parameters, ParameterEquivalenceComparer.Default)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0005, member.Locations.First()), member); + if (!methodSymbol.Parameters.SequenceEqual(syncMember.Parameters, ParameterEquivalenceComparer.Default)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0005, member.Locations.First()), member); } CheckClientMethod(context, syncMember); From 1ea800a89ef87cc5d7cd62f825cd442641238bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Mon, 9 Dec 2024 13:50:39 -0800 Subject: [PATCH 06/11] Revert "Skip check for AMQP libraries" This reverts commit 0b4834b992f110d0f2bedc5f9e0d5b801fa042db. --- .../ClientMethodsAnalyzer.cs | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index 23c6a91a01e..9186cb72806 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -295,31 +295,9 @@ private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol return false; } - private bool IsAmqpBasedLibrary(INamedTypeSymbol type) - { - var namespaceName = type.ContainingNamespace.ToDisplayString(); - - // List of AMQP-based libraries that are exempted from this rule. - if (namespaceName.StartsWith("Azure.Messaging.EventHubs") || - namespaceName.StartsWith("Azure.Messaging.ServiceBus") || - namespaceName.StartsWith("Azure.Messaging.WebPubSub")) - { - return true; - } - - return false; - } - public override void AnalyzeCore(ISymbolAnalysisContext context) { INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; - - // Skip the verification for AMQP-based SDKs - if (IsAmqpBasedLibrary(type)) - { - return; - } - foreach (var member in type.GetMembers()) { var methodSymbol = member as IMethodSymbol; @@ -347,9 +325,9 @@ public override void AnalyzeCore(ISymbolAnalysisContext context) context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member); } - if (!methodSymbol.Parameters.SequenceEqual(syncMember.Parameters, ParameterEquivalenceComparer.Default)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0005, member.Locations.First()), member); + if (!methodSymbol.Parameters.SequenceEqual(syncMember.Parameters, ParameterEquivalenceComparer.Default)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0005, member.Locations.First()), member); } CheckClientMethod(context, syncMember); From 10a5a0a841b2280db0120e52899e198d4cea4881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Mon, 9 Dec 2024 17:00:46 -0800 Subject: [PATCH 07/11] Fix AZC0004 tests --- .../ClientMethodsAnalyzer.cs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index 9186cb72806..11351c7fccc 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -286,13 +286,36 @@ private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol return false; } + // If the async return type is non-generic Task + if (asyncReturnType.Name == TaskTypeName && !((INamedTypeSymbol)asyncReturnType).IsGenericType) + { + if (syncReturnType.Name == TaskTypeName && !((INamedTypeSymbol)syncReturnType).IsGenericType) + { + return true; + } + else + { + // Async method returns Task, sync method should return void + return syncReturnType.SpecialType == SpecialType.System_Void; + } + } + if (asyncReturnType is INamedTypeSymbol asyncNamedType && asyncNamedType.IsGenericType && asyncNamedType.Name == TaskTypeName) { - var asyncInnerType = asyncNamedType.TypeArguments.Single(); - return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); + if (syncReturnType is INamedTypeSymbol syncNamedType && syncNamedType.IsGenericType && syncNamedType.Name == TaskTypeName) + { + var asyncInnerType = asyncNamedType.TypeArguments.Single(); + var syncInnerType = syncNamedType.TypeArguments.Single(); + return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncInnerType); + } + else + { + var asyncInnerType = asyncNamedType.TypeArguments.FirstOrDefault(); + return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); + } } - return false; + return SymbolEqualityComparer.Default.Equals(asyncReturnType, syncReturnType); } public override void AnalyzeCore(ISymbolAnalysisContext context) From b86efc47198e5b18732789c1f990d6413e3bc39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Wed, 11 Dec 2024 12:43:26 -0800 Subject: [PATCH 08/11] handle scenario for Pageable types --- .../ClientMethodsAnalyzer.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index 11351c7fccc..58f998ae732 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -311,7 +311,22 @@ private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol else { var asyncInnerType = asyncNamedType.TypeArguments.FirstOrDefault(); + + string asyncInnerTypeName = asyncNamedType.Name; + return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); + + } + } + + // Add scenario to handle AsyncPageable and Pageable return types + if (asyncReturnType is INamedTypeSymbol asyncNamedTypeSymbol && asyncNamedTypeSymbol.IsGenericType && asyncNamedTypeSymbol.Name == AsyncPageableTypeName) + { + if (syncReturnType is INamedTypeSymbol syncNamedTypeSymbol && syncNamedTypeSymbol.IsGenericType && syncNamedTypeSymbol.Name == PageableTypeName) + { + var asyncInnerType = asyncNamedTypeSymbol.TypeArguments.Single(); + var syncInnerType = syncNamedTypeSymbol.TypeArguments.Single(); + return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncInnerType); } } From 8705bd89b77f1bc724847c7240bdcf139c102dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Wed, 11 Dec 2024 13:55:08 -0800 Subject: [PATCH 09/11] Compare return types recursively --- .../ClientMethodsAnalyzer.cs | 94 +++++++++++++------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index 58f998ae732..deb427f75fa 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -279,58 +279,90 @@ private static bool IsClientMethodReturnType(ISymbolAnalysisContext context, IMe return false; } - private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol syncReturnType) + private static bool CompareReturnTypesRecursively(ITypeSymbol asyncType, ITypeSymbol syncType) { - if (asyncReturnType == null || syncReturnType == null) - { - return false; - } - - // If the async return type is non-generic Task - if (asyncReturnType.Name == TaskTypeName && !((INamedTypeSymbol)asyncReturnType).IsGenericType) + // Unwrap Task for async methods + if (asyncType is INamedTypeSymbol asyncNamedType && asyncNamedType.Name == TaskTypeName) { - if (syncReturnType.Name == TaskTypeName && !((INamedTypeSymbol)syncReturnType).IsGenericType) + if (asyncNamedType.IsGenericType) { - return true; + asyncType = asyncNamedType.TypeArguments.Single(); } else { - // Async method returns Task, sync method should return void - return syncReturnType.SpecialType == SpecialType.System_Void; + // async returns Task, sync should return void or non-generic Task + if (syncType.SpecialType == SpecialType.System_Void || + (syncType is INamedTypeSymbol syncNamedType && syncNamedType.Name == TaskTypeName && !syncNamedType.IsGenericType)) + { + return true; + } + return false; } } - if (asyncReturnType is INamedTypeSymbol asyncNamedType && asyncNamedType.IsGenericType && asyncNamedType.Name == TaskTypeName) + // Map AsyncPageable to Pageable for easy comparison since they are equivalent for these purposes + if (asyncType is INamedTypeSymbol asyncTypeSymbol && asyncTypeSymbol.Name == AsyncPageableTypeName && asyncTypeSymbol.IsGenericType) { - if (syncReturnType is INamedTypeSymbol syncNamedType && syncNamedType.IsGenericType && syncNamedType.Name == TaskTypeName) + asyncType = asyncTypeSymbol.ContainingNamespace.GetTypeMembers(PageableTypeName).FirstOrDefault()?.Construct(asyncTypeSymbol.TypeArguments.ToArray()); + } + + // Compare directly if sync method return type is not a named type symbol + if (syncType is not INamedTypeSymbol syncTypeSymbol) + { + return SymbolEqualityComparer.Default.Equals(asyncType, syncType); + } + + // Compare type names and namespaces + if (asyncType is INamedTypeSymbol asyncNamedTypeSymbol && syncType is INamedTypeSymbol syncNamedTypeSymbol) + { + if (asyncNamedTypeSymbol.Name != syncNamedTypeSymbol.Name || + asyncNamedTypeSymbol.ContainingNamespace.ToDisplayString() != syncNamedTypeSymbol.ContainingNamespace.ToDisplayString()) { - var asyncInnerType = asyncNamedType.TypeArguments.Single(); - var syncInnerType = syncNamedType.TypeArguments.Single(); - return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncInnerType); + return false; } - else - { - var asyncInnerType = asyncNamedType.TypeArguments.FirstOrDefault(); - string asyncInnerTypeName = asyncNamedType.Name; + // Compare nested types recursively + if (asyncNamedTypeSymbol.IsGenericType && syncNamedTypeSymbol.IsGenericType) + { + var asyncTypeArguments = asyncNamedTypeSymbol.TypeArguments; + var syncTypeArguments = syncNamedTypeSymbol.TypeArguments; - return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncReturnType); + if (asyncTypeArguments.Length != syncTypeArguments.Length) + { + return false; + } + for (int i = 0; i < asyncTypeArguments.Length; i++) + { + if (!CompareReturnTypesRecursively(asyncTypeArguments[i], syncTypeArguments[i])) + { + return false; + } + } + return true; + } + else if (!asyncNamedTypeSymbol.IsGenericType && !syncNamedTypeSymbol.IsGenericType) + { + return true; + } + else + { + // One is a generic type and the other is not + return false; } } - // Add scenario to handle AsyncPageable and Pageable return types - if (asyncReturnType is INamedTypeSymbol asyncNamedTypeSymbol && asyncNamedTypeSymbol.IsGenericType && asyncNamedTypeSymbol.Name == AsyncPageableTypeName) + return SymbolEqualityComparer.Default.Equals(asyncType, syncType); + } + + private static bool DoReturnTypesMatch(ITypeSymbol asyncReturnType, ITypeSymbol syncReturnType) + { + if (asyncReturnType == null || syncReturnType == null) { - if (syncReturnType is INamedTypeSymbol syncNamedTypeSymbol && syncNamedTypeSymbol.IsGenericType && syncNamedTypeSymbol.Name == PageableTypeName) - { - var asyncInnerType = asyncNamedTypeSymbol.TypeArguments.Single(); - var syncInnerType = syncNamedTypeSymbol.TypeArguments.Single(); - return SymbolEqualityComparer.Default.Equals(asyncInnerType, syncInnerType); - } + return false; } - return SymbolEqualityComparer.Default.Equals(asyncReturnType, syncReturnType); + return CompareReturnTypesRecursively(asyncReturnType, syncReturnType); } public override void AnalyzeCore(ISymbolAnalysisContext context) From 4d655ab1dddfb901106c09275473043015eb4b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Wed, 11 Dec 2024 14:01:00 -0800 Subject: [PATCH 10/11] nit: scope everything with the not operator --- .../Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs index deb427f75fa..78e2674375c 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -341,7 +341,7 @@ private static bool CompareReturnTypesRecursively(ITypeSymbol asyncType, ITypeSy } return true; } - else if (!asyncNamedTypeSymbol.IsGenericType && !syncNamedTypeSymbol.IsGenericType) + else if ((!asyncNamedTypeSymbol.IsGenericType) && (!syncNamedTypeSymbol.IsGenericType)) { return true; } From 90e9d4c4ed0601939ac63f8db14881bc2bbb7002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20C=C3=A1rdenas?= Date: Wed, 11 Dec 2024 15:03:17 -0800 Subject: [PATCH 11/11] Add tests --- .../AZC0004Tests.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0004Tests.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0004Tests.cs index d048a9e7f62..f95409213b4 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0004Tests.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0004Tests.cs @@ -396,5 +396,60 @@ public virtual Response Get(CancellationToken cancellationToken) await Verifier.CreateAnalyzer(code) .RunAsync(); } + + [Fact] + public async Task AZC0004ProducedForMethodsWithMismatchedReturnTypes() + { + const string code = @" +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task {|AZC0004:GetAsync|}(CancellationToken cancellationToken = default) + { + return null; + } + + public virtual int Get(CancellationToken cancellationToken = default) + { + return 0; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .WithDisabledDiagnostics("AZC0015") + .RunAsync(); + } + + [Fact] + public async Task AZC0004ProducedForMethotsWithMismatchedParameters() + { + const string code = @" +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task {|AZC0004:GetAsync|}(CancellationToken cancellationToken = default) + { + return null; + } + + public virtual string Get(string foo, CancellationToken cancellationToken = default) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .WithDisabledDiagnostics("AZC0015") + .RunAsync(); + } + } }