From d492a77c7799e4c5e2a474ef8efa50cd3e947998 Mon Sep 17 00:00:00 2001 From: pshao25 <97225342+pshao25@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:13:27 +0800 Subject: [PATCH] Add rules for convenience method and protocol method --- .../AZC0004Tests.cs | 34 +- .../AZC0017Tests.cs | 64 +++ .../AZC0018Tests.cs | 367 ++++++++++++++++++ .../AZC0019Tests.cs | 51 +++ .../ClientMethodsAnalyzer.cs | 163 +++++++- .../Azure.ClientSdk.Analyzers/Descriptors.cs | 18 + 6 files changed, 689 insertions(+), 8 deletions(-) create mode 100644 src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0017Tests.cs create mode 100644 src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0018Tests.cs create mode 100644 src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0019Tests.cs 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 ca9cddf34a5..d048a9e7f62 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 @@ -364,5 +364,37 @@ await Verifier.CreateAnalyzer(code) .WithDisabledDiagnostics("AZC0015") .RunAsync(); } + + [Fact] + public async Task AZC0004NotProducedForMethodsWithOverloadAlternative() + { + const string code = @" +using Azure; +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task GetAsync(CancellationToken cancellationToken = default) + { + return null; + } + + public virtual Response Get() + { + return null; + } + + public virtual Response Get(CancellationToken cancellationToken) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } } -} \ No newline at end of file +} diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0017Tests.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0017Tests.cs new file mode 100644 index 00000000000..e4b33099ec5 --- /dev/null +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0017Tests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Xunit; +using Verifier = Azure.ClientSdk.Analyzers.Tests.AzureAnalyzerVerifier; + +namespace Azure.ClientSdk.Analyzers.Tests +{ + public class AZC0017Tests + { + [Fact] + public async Task AZC0017ProducedForMethodsWithRequestContentParameter() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task {|AZC0017:GetAsync|}(RequestContent content, CancellationToken cancellationToken = default) + { + return null; + } + public virtual Response {|AZC0017:Get|}(RequestContent content, CancellationToken cancellationToken = default) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0017NotProducedForMethodsWithCancellationToken() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task GetAsync(string s, CancellationToken cancellationToken = default) + { + return null; + } + public virtual Response Get(string s, CancellationToken cancellationToken = default) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + } +} diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0018Tests.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0018Tests.cs new file mode 100644 index 00000000000..f4bf3e5c723 --- /dev/null +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0018Tests.cs @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Xunit; +using Verifier = Azure.ClientSdk.Analyzers.Tests.AzureAnalyzerVerifier; + +namespace Azure.ClientSdk.Analyzers.Tests +{ + public class AZC0018Tests + { + [Fact] + public async Task AZC0018NotProducedForCorrectReturnType() + { + const string code = @" +using Azure; +using Azure.Core; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task> GetHeadAsBooleanAsync(string s, RequestContext context) + { + return null; + } + + public virtual Response GetHeadAsBoolean(string s, RequestContext context) + { + return null; + } + + public virtual Task GetResponseAsync(string s, RequestContext context) + { + return null; + } + + public virtual Response GetResponse(string s, RequestContext context) + { + return null; + } + + public virtual AsyncPageable GetPageableAsync(string s, RequestContext context) + { + return null; + } + + public virtual Pageable GetPageable(string s, RequestContext context) + { + return null; + } + + public virtual Task GetOperationAsync(string s, RequestContext context) + { + return null; + } + + public virtual Operation GetOperation(string s, RequestContext context) + { + return null; + } + + public virtual Task> GetOperationOfTAsync(string s, RequestContext context) + { + return null; + } + + public virtual Operation GetOperationOfT(string s, RequestContext context) + { + return null; + } + + public virtual Task>> GetOperationOfPageableAsync(string s, RequestContext context) + { + return null; + } + + public virtual Operation> GetOperationOfPageable(string s, RequestContext context) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018ProducedForMethodsWithGenericResponseOfPrimitive() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task> {|AZC0018:GetAsync|}(string s, RequestContext context) + { + return null; + } + + public virtual Response {|AZC0018:Get|}(string s, RequestContext context) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018ProducedForMethodsWithGenericResponseOfModel() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class Model + { + string a; + } + public class SomeClient + { + public virtual Task> {|AZC0018:GetAsync|}(string s, RequestContext context) + { + return null; + } + + public virtual Response {|AZC0018:Get|}(string s, RequestContext context) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018ProducedForMethodsWithPageableOfModel() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class Model + { + string a; + } + public class SomeClient + { + public virtual AsyncPageable {|AZC0018:GetAsync|}(string s, RequestContext context) + { + return null; + } + + public virtual Pageable {|AZC0018:Get|}(string s, RequestContext context) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018ProducedForMethodsWithOperationOfModel() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class Model + { + string a; + } + public class SomeClient + { + public virtual Task> {|AZC0018:GetAsync|}(string s, RequestContext context) + { + return null; + } + + public virtual Operation {|AZC0018:Get|}(string s, RequestContext context) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018ProducedForMethodsWithOperationOfPageableModel() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; + +namespace RandomNamespace +{ + public class Model + { + string a; + } + public class SomeClient + { + public virtual Task>> {|AZC0018:GetAsync|}(string s, RequestContext context) + { + return null; + } + + public virtual Operation> {|AZC0018:Get|}(string s, RequestContext context) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018ProducedForMethodsWithParameterModel() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RandomNamespace +{ + public struct Model + { + string a; + } + public class SomeClient + { + public virtual Task {|AZC0018:GetAsync|}(Model model, Azure.RequestContext context) + { + return null; + } + + public virtual Response {|AZC0018:Get|}(Model model, Azure.RequestContext context) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018NotProducedForMethodsWithNoRequestContentAndRequiredContext() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task GetAsync(string a, Azure.RequestContext context) + { + return null; + } + + public virtual Response Get(string a, Azure.RequestContext context) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018NotProducedForMethodsWithNoRequestContentButOnlyProtocol() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task GetAsync(string a, Azure.RequestContext context = null) + { + return null; + } + + public virtual Response Get(string a, Azure.RequestContext context = null) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + + [Fact] + public async Task AZC0018NotProducedForMethodsWithRequestContentAndOptionalRequestContext() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task GetAsync(RequestContent content, RequestContext context = null) + { + return null; + } + + public virtual Response Get(RequestContent content, RequestContext context = null) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + } +} diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0019Tests.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0019Tests.cs new file mode 100644 index 00000000000..c6626adf639 --- /dev/null +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/AZC0019Tests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Xunit; +using Verifier = Azure.ClientSdk.Analyzers.Tests.AzureAnalyzerVerifier; + +namespace Azure.ClientSdk.Analyzers.Tests +{ + public class AZC0019Tests + { + [Fact] + public async Task AZC0019ProducedForMethodsWithNoRequestContentButProtocolAndConvenience() + { + const string code = @" +using Azure; +using Azure.Core; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RandomNamespace +{ + public class SomeClient + { + public virtual Task GetAsync(string a, CancellationToken cancellationToken = default) + { + return null; + } + + public virtual Response Get(string a, CancellationToken cancellationToken = default) + { + return null; + } + + public virtual Task {|AZC0019:GetAsync|}(string a, Azure.RequestContext context = null) + { + return null; + } + + public virtual Response {|AZC0019:Get|}(string a, Azure.RequestContext context = null) + { + return null; + } + } +}"; + await Verifier.CreateAnalyzer(code) + .RunAsync(); + } + } +} 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 d771f2404cd..d9f52413c89 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ClientMethodsAnalyzer.cs @@ -1,6 +1,7 @@ // 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; @@ -15,19 +16,29 @@ public class ClientMethodsAnalyzer : ClientAnalyzerBase 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.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"; @@ -43,7 +54,7 @@ private static bool IsCancellationOrRequestContext(IParameterSymbol parameterSym return IsCancellationToken(parameterSymbol) || IsRequestContext(parameterSymbol); } - private static void CheckIsLastArgumentCancellationTokenOrRequestContext(ISymbolAnalysisContext context, IMethodSymbol member) + private static void CheckServiceMethod(ISymbolAnalysisContext context, IMethodSymbol member) { var lastArgument = member.Parameters.LastOrDefault(); var isLastArgumentCancellationOrRequestContext = lastArgument != null && IsCancellationOrRequestContext(lastArgument); @@ -61,20 +72,158 @@ private static void CheckIsLastArgumentCancellationTokenOrRequestContext(ISymbol context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); } } - else if (IsCancellationToken(lastArgument) && !lastArgument.IsOptional) + else if (IsCancellationToken(lastArgument)) { - var overloadWithCancellationToken = FindMethod( + if (!lastArgument.IsOptional) + { + var overloadWithCancellationToken = FindMethod( member.ContainingType.GetMembers(member.Name).OfType(), member.TypeParameters, member.Parameters.RemoveAt(member.Parameters.Length - 1)); - if (overloadWithCancellationToken == null) + if (overloadWithCancellationToken == null) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); + } + } + + if (member.Parameters.FirstOrDefault(IsRequestContent) != null) { - context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0002, member.Locations.FirstOrDefault()), member); + 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; + string currentName = currentNamespace.Name; + string fullNamespace = ""; + while (!string.IsNullOrEmpty(currentName)) + { + fullNamespace = fullNamespace == "" ? currentName : $"{currentName}.{fullNamespace}"; + currentNamespace = currentNamespace.ContainingNamespace; + 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) + { + if ((!IsOrImplements(typeSymbol, PageableTypeName)) && (!IsOrImplements(typeSymbol, AsyncPageableTypeName))) + { + return false; + } + + var pageableTypeSymbol = typeSymbol as INamedTypeSymbol; + if (!pageableTypeSymbol.IsGenericType) + { + return false; + } + + var pageableReturn = pageableTypeSymbol.TypeArguments.Single(); + if (!IsOrImplements(pageableReturn, BinaryDataTypeName)) + { + 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)) + { + 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)) + { + if (unwrappedType is INamedTypeSymbol operationTypeSymbol && operationTypeSymbol.IsGenericType) + { + var operationReturn = operationTypeSymbol.TypeArguments.Single(); + if (IsOrImplements(operationReturn, PageableTypeName) || IsOrImplements(operationReturn, AsyncPageableTypeName)) + { + if (!IsValidPageable(operationReturn)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + return; + } + + if (!IsOrImplements(operationReturn, BinaryDataTypeName)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptors.AZC0018, method.Locations.FirstOrDefault()), method); + } + } + return; + } + else if (IsOrImplements(originalType, PageableTypeName) || IsOrImplements(originalType, AsyncPageableTypeName)) + { + 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); @@ -164,7 +313,7 @@ public override void AnalyzeCore(ISymbolAnalysisContext context) if (IsClientMethodReturnType(context, methodSymbol, false)) { - CheckIsLastArgumentCancellationTokenOrRequestContext(context, methodSymbol); + CheckServiceMethod(context, methodSymbol); } } } diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/Descriptors.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/Descriptors.cs index 0b292863d5b..84d1dd64d6d 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/Descriptors.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/Descriptors.cs @@ -112,6 +112,24 @@ internal class Descriptors "Usage", DiagnosticSeverity.Warning, true); + public static DiagnosticDescriptor AZC0017 = new DiagnosticDescriptor( + nameof(AZC0017), + "Invalid convenience method signature.", + "Convenience method shouldn't have prameters with type RequestContent.", + "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: null); + + public static DiagnosticDescriptor AZC0018 = new DiagnosticDescriptor( + nameof(AZC0018), + "Invalid protocol method signature.", + "Protocol method should take a RequestContext parameter called `context` and not use a model as parameter or return types.", + "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: null); + + public static DiagnosticDescriptor AZC0019 = new DiagnosticDescriptor( + nameof(AZC0019), + "Potential ambiguous call exists.", + "There will be ambiguous call when user calls with required parameters. All parameters of the protocol method should be required.", + "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: null); + public static DiagnosticDescriptor AZC0020 = new DiagnosticDescriptor( nameof(AZC0020), "Avoid using banned types in public APIs",