Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
pshao25 committed Aug 3, 2023
1 parent 14554b1 commit 040cd84
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -295,5 +295,43 @@ await Verifier.CreateAnalyzer(code)
.WithDisabledDiagnostics("AZC0018")
.RunAsync();
}

[Fact]
public async Task AZC0002NotProducedIfThereIsAnOverloadWithCancellationToken()
{
const string code = @"
using Azure;
using System.Threading;
using System.Threading.Tasks;
namespace RandomNamespace
{
public class SomeClient
{
public virtual Response GetAsync(string s)
{
return null;
}
public virtual Response Get(string s)
{
return null;
}
public virtual Response GetAsync(string s, CancellationToken cancellationToken)
{
return null;
}
public virtual Response Get(string s, CancellationToken cancellationToken)
{
return null;
}
}
}";
await Verifier.CreateAnalyzer(code)
.WithDisabledDiagnostics("AZC0015")
.RunAsync();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public class AssetConversion {}
public class SomeClient
{
public virtual Task<Response<bool>> GetHeadAsBooleanAsync(string s, RequestContext context)
{
return null;
}
public virtual Response<bool> GetHeadAsBoolean(string s, RequestContext context)
{
return null;
}
public virtual Task<Response> GetResponseAsync(string s, RequestContext context)
{
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ClientMethodsAnalyzer : ClientAnalyzerBase
private const string NullableResponseTypeName = "NullableResponse";
private const string OperationTypeName = "Operation";
private const string TaskTypeName = "Task";
private const string BooleanTypeName = "Boolean";

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(new[]
{
Expand All @@ -45,7 +46,7 @@ private static bool IsRequestContext(IParameterSymbol parameterSymbol)

private static bool IsCancellationToken(IParameterSymbol parameterSymbol)
{
return parameterSymbol.Name == "cancellationToken" && parameterSymbol.Type.Name == "CancellationToken" && parameterSymbol.IsOptional;
return parameterSymbol.Name == "cancellationToken" && parameterSymbol.Type.Name == "CancellationToken";
}

private static void CheckClientMethod(ISymbolAnalysisContext context, IMethodSymbol member)
Expand All @@ -67,10 +68,31 @@ static bool IsCancellationOrRequestContext(IParameterSymbol parameterSymbol)

if (!isCancellationOrRequestContext)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member);
var overloadSupportsCancellations = FindMethod(
member.ContainingType.GetMembers(member.Name).OfType<IMethodSymbol>(),
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<IMethodSymbol>(),
member.TypeParameters,
member.Parameters.RemoveAt(member.Parameters.Length - 1));

if (overloadWithCancellationToken == null)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member);
}
}
// A convenience method should not have RequestContent as parameter
if (member.Parameters.FirstOrDefault(IsRequestContent) != null)
{
Expand Down Expand Up @@ -129,7 +151,7 @@ private static void CheckProtocolMethodParameters(ISymbolAnalysisContext context
}
}

// A protocol method should not have model as type. Accepted return type: Response, Task<Response>, Pageable<BinaryData>, AsyncPageable<BinaryData>, Operation<BinaryData>, Task<Operation<BinaryData>>, Operation, Task<Operation>, Operation<Pageable<BinaryData>>, Task<Operation<AsyncPageable<BinaryData>>>
// A protocol method should not have model as type. Accepted return type: Response, Task<Response>, Response<bool>, Task<Response<bool>>, Pageable<BinaryData>, AsyncPageable<BinaryData>, Operation<BinaryData>, Task<Operation<BinaryData>>, Operation, Task<Operation>, Operation<Pageable<BinaryData>>, Task<Operation<AsyncPageable<BinaryData>>>
private static void CheckProtocolMethodReturnType(ISymbolAnalysisContext context, IMethodSymbol method)
{
bool IsValidPageable(ITypeSymbol typeSymbol)
Expand Down Expand Up @@ -168,7 +190,11 @@ bool IsValidPageable(ITypeSymbol typeSymbol)
{
if (unwrappedType is INamedTypeSymbol responseTypeSymbol && responseTypeSymbol.IsGenericType)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method);
var responseReturn = responseTypeSymbol.TypeArguments.Single();
if (responseReturn.Name != BooleanTypeName)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method);
}
}
return;
}
Expand Down Expand Up @@ -264,32 +290,45 @@ private bool IsCheckExempt(ISymbolAnalysisContext context, IMethodSymbol method)

public override void AnalyzeCore(ISymbolAnalysisContext context)
{
void CheckSyncAsyncPair(INamedTypeSymbol type, IMethodSymbol method, string methodName)
{
var lastArgument = method.Parameters.LastOrDefault();
IMethodSymbol methodSymbol = null;
if (lastArgument != null && IsRequestContext(lastArgument))
{
methodSymbol = FindMethod(type.GetMembers(methodName).OfType<IMethodSymbol>(), method.TypeParameters, method.Parameters);
}
else if (lastArgument != null && IsCancellationToken(lastArgument))
{
methodSymbol = FindMethod(type.GetMembers(methodName).OfType<IMethodSymbol>(), method.TypeParameters, method.Parameters.RemoveAt(method.Parameters.Length - 1), p => IsCancellationToken(p));
}
else
{
return;
}

if (methodSymbol == null)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, method.Locations.First()), method);
}
}

INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol;
foreach (var member in type.GetMembers())
{
if (member is IMethodSymbol asyncMethodSymbol && !IsCheckExempt(context, asyncMethodSymbol) && asyncMethodSymbol.Name.EndsWith(AsyncSuffix))
{
var syncMemberName = member.Name.Substring(0, member.Name.Length - AsyncSuffix.Length);
var syncMember = FindMethod(type.GetMembers(syncMemberName).OfType<IMethodSymbol>(), asyncMethodSymbol.TypeParameters, asyncMethodSymbol.Parameters);

if (syncMember == null)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member);
}

CheckClientMethod(context, asyncMethodSymbol);

var syncMemberName = member.Name.Substring(0, member.Name.Length - AsyncSuffix.Length);
CheckSyncAsyncPair(type, asyncMethodSymbol, syncMemberName);
}
else if (member is IMethodSymbol syncMethodSymbol && !IsCheckExempt(context, syncMethodSymbol) && !syncMethodSymbol.Name.EndsWith(AsyncSuffix))
{
var asyncMemberName = member.Name + AsyncSuffix;
var asyncMember = FindMethod(type.GetMembers(asyncMemberName).OfType<IMethodSymbol>(), syncMethodSymbol.TypeParameters, syncMethodSymbol.Parameters);

if (asyncMember == null)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0004, member.Locations.First()), member);
}

CheckClientMethod(context, syncMethodSymbol);

var asyncMemberName = member.Name + AsyncSuffix;
CheckSyncAsyncPair(type, syncMethodSymbol, asyncMemberName);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal class Descriptors
public static DiagnosticDescriptor AZC0002 = new DiagnosticDescriptor(
nameof(AZC0002),
"DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called cancellationToken or a RequestContext parameter called context.",
"Client method should have an optional CancellationToken (both name and it being optional matters) or a RequestContext as the last parameter.",
"Client method should have an optional CancellationToken called cancellationToken (both name and it being optional matters) or a RequestContext called context as the last parameter.",
"Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: null,
"https://azure.github.io/azure-sdk/dotnet_introduction.html#dotnet-service-methods-cancellation"
);
Expand Down

0 comments on commit 040cd84

Please sign in to comment.