diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/ModelName/AZC0030OptionsTest.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/ModelName/AZC0030OptionsTest.cs new file mode 100644 index 00000000000..d4009346519 --- /dev/null +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers.Tests/ModelName/AZC0030OptionsTest.cs @@ -0,0 +1,93 @@ +using System.Threading.Tasks; +using Xunit; + +using VerifyCS = Azure.ClientSdk.Analyzers.Tests.AzureAnalyzerVerifier< + Azure.ClientSdk.Analyzers.ModelName.GeneralSuffixAnalyzer>; + +// Test cases for `Options` suffix. We allow `Options` suffix for property bags. +// So need to skip property bags which do not have any serialization codes. +namespace Azure.ClientSdk.Analyzers.Tests.ModelName +{ + public class AZC0030OptionsSuffixTests + { + private const string diagnosticId = "AZC0030"; + + [Fact] + public async Task WithoutAnySerialization() + { + var test = @"using System.Text.Json; +namespace Azure.ResourceManager.Models; + +public class DiskOptions +{ +}"; + await VerifyCS.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task WithDeserializationMethod() + { + var test = @"using System.Text.Json; +namespace Azure.ResourceManager +{ + public class ResponseOptions + { + public static ResponseOptions DeserializeResponseOptions(JsonElement element) + { + return null; + } + } +}"; + var expected = VerifyCS.Diagnostic(diagnosticId).WithSpan(4, 18, 4, 33).WithArguments("ResponseOptions", "Options", "'ResponseConfig'"); + await VerifyCS.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task WithSerializationMethod() + { + var test = @"using System.Text.Json; +namespace Azure.ResourceManager.Models +{ + internal interface IUtf8JsonSerializable + { + void Write(Utf8JsonWriter writer); + }; + + public class DiskOptions: IUtf8JsonSerializable + { + void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) {} + } +}"; + var expected = VerifyCS.Diagnostic(diagnosticId).WithSpan(9, 18, 9, 29).WithArguments("DiskOptions", "Options", "'DiskConfig'"); + await VerifyCS.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task WithPublicSerialization() + { + var test = @"using System.Text.Json; +namespace Azure.ResourceManager.Models +{ + public class ModelReaderWriterOptions + { + public string Foo; + } + + public interface IJsonModel + { + void Write(Utf8JsonWriter writer, ModelReaderWriterOptions options); + }; + + namespace SubTest + { + public class DiskOptions: IJsonModel + { + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) {} + } + } +}"; + var expected = VerifyCS.Diagnostic(diagnosticId).WithSpan(16, 22, 16, 33).WithArguments("DiskOptions", "Options", "'DiskConfig'"); + await VerifyCS.VerifyAnalyzerAsync(test, expected); + } + } +} diff --git a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ModelName/GeneralSuffixAnalyzer.cs b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ModelName/GeneralSuffixAnalyzer.cs index ade8cc58d01..28782bd1acd 100644 --- a/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ModelName/GeneralSuffixAnalyzer.cs +++ b/src/dotnet/Azure.ClientSdk.Analyzers/Azure.ClientSdk.Analyzers/ModelName/GeneralSuffixAnalyzer.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; -using System.Text.RegularExpressions; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -18,11 +18,41 @@ public class GeneralSuffixAnalyzer : SuffixAnalyzerBase private static readonly ImmutableHashSet reservedNames = ImmutableHashSet.Create("ErrorResponse"); // Avoid to use suffixes "Request(s)", "Parameter(s)", "Option(s)", "Response(s)", "Collection" - private static readonly string[] generalSuffixes = new string[] { "Request", "Requests", "Response", "Responses", "Parameter", "Parameters", "Option", "Options", "Collection"}; + private static readonly string[] generalSuffixes = new string[] { "Request", "Requests", "Response", "Responses", "Parameter", "Parameters", "Option", "Options", "Collection" }; public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Descriptors.AZC0030); } } - protected override bool ShouldSkip(INamedTypeSymbol symbol, SymbolAnalysisContext context) => reservedNames.Contains(symbol.Name); + protected override bool ShouldSkip(INamedTypeSymbol symbol, SymbolAnalysisContext context) + { + if (reservedNames.Contains(symbol.Name)) + return true; + + // skip property bag classes which have `Options` suffix and don't have serialization + if (symbol.Name.EndsWith("Options") && !SupportSerialization(symbol)) + return true; + + return false; + } + + private bool SupportSerialization(INamedTypeSymbol symbol) + { + // if it has serialization method: `IUtf8JsonSerializable.Write`, e.g. ": IUtf8JsonSerializable" + if (symbol.Interfaces.Any(i => i.Name is "IUtf8JsonSerializable")) + return true; + + // if it has deserialization method: static Deserialize(JsonElement element) + if (symbol.GetMembers($"Deserialize{symbol.Name}").Any(m => m is IMethodSymbol methodSymbol && + methodSymbol is { IsStatic: true, ReturnType: INamedTypeSymbol symbol, Parameters.Length: 1 } && + methodSymbol.Parameters[0].Type.Name is "JsonElement")) + return true; + + // if it has public serialization: IJsonModel + if (symbol.Interfaces.Any(i => i is { Name: "IJsonModel", IsGenericType: true, TypeArguments.Length: 1 } && + i.TypeArguments[0] == symbol)) + return true; + + return false; + } protected override string[] SuffixesToCatch => generalSuffixes; protected override Diagnostic GetDiagnostic(INamedTypeSymbol typeSymbol, string suffix, SymbolAnalysisContext context)