diff --git a/analyzers/rspec/cs/S6424_c#.html b/analyzers/rspec/cs/S6424_c#.html index 4adc64d841d..0fa0e724569 100644 --- a/analyzers/rspec/cs/S6424_c#.html +++ b/analyzers/rspec/cs/S6424_c#.html @@ -8,8 +8,8 @@
If any of these rules are violated, an InvalidOperationException
is thrown at runtime when the interface is used as a type argument to
-IDurableClient.SignalEntityAsync<T>
or IDurableOrchestrationContext.CreateEntityProxy<T>
. The exception message
-explains which rule was broken.
IDurableEntityContext.SignalEntity<TEntityInterface>
, IDurableEntityClient.SignalEntityAsync<TEntityInterface>
+or IDurableOrchestrationContext.CreateEntityProxy<TEntityInterface>
. The exception message explains which rule was broken.
This rule raises an issue in case any of the restrictions above is not respected.
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/CloudNative/DurableEntityInterfaceRestrictions.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/CloudNative/DurableEntityInterfaceRestrictions.cs index 5248bb28d34..db5f1d1fab1 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/CloudNative/DurableEntityInterfaceRestrictions.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/CloudNative/DurableEntityInterfaceRestrictions.cs @@ -33,6 +33,7 @@ public sealed class DurableEntityInterfaceRestrictions : SonarDiagnosticAnalyzer { private const string DiagnosticId = "S6424"; private const string MessageFormat = "Use valid entity interface. {0} {1}."; + private const string SignalEntityName = "SignalEntity"; private const string SignalEntityAsyncName = "SignalEntityAsync"; private const string CreateEntityProxyName = "CreateEntityProxy"; @@ -44,11 +45,10 @@ protected override void Initialize(SonarAnalysisContext context) => context.RegisterSyntaxNodeActionInNonGenerated(c => { var name = (GenericNameSyntax)c.Node; - if (name.Identifier.ValueText is SignalEntityAsyncName or CreateEntityProxyName + if (name.Identifier.ValueText is SignalEntityName or SignalEntityAsyncName or CreateEntityProxyName && name.TypeArgumentList.Arguments.Count == 1 && c.SemanticModel.GetSymbolInfo(name).Symbol is IMethodSymbol method - && (method.Is(KnownType.Microsoft_Azure_WebJobs_Extensions_DurableTask_IDurableEntityClient, SignalEntityAsyncName) - || method.Is(KnownType.Microsoft_Azure_WebJobs_Extensions_DurableTask_IDurableOrchestrationContext, CreateEntityProxyName)) + && IsRestrictedMethod(method) && method.TypeArguments.Single() is INamedTypeSymbol { TypeKind: not TypeKind.Error } entityInterface && InterfaceErrorMessage(entityInterface) is { } message) { @@ -57,6 +57,11 @@ protected override void Initialize(SonarAnalysisContext context) => }, SyntaxKind.GenericName); + private static bool IsRestrictedMethod(IMethodSymbol method) => + method.Is(KnownType.Microsoft_Azure_WebJobs_Extensions_DurableTask_IDurableEntityContext, SignalEntityName) + || method.Is(KnownType.Microsoft_Azure_WebJobs_Extensions_DurableTask_IDurableEntityClient, SignalEntityAsyncName) + || method.Is(KnownType.Microsoft_Azure_WebJobs_Extensions_DurableTask_IDurableOrchestrationContext, CreateEntityProxyName); + private static string InterfaceErrorMessage(INamedTypeSymbol entityInterface) { if (entityInterface.IsGenericType) @@ -86,15 +91,15 @@ private static string MemberErrorMessage(ISymbol member) { return $@"contains method ""{method.Name}"" with {method.Parameters.Length} parameters. Zero or one are allowed"; } - else if (method.ReturnsVoid + else if (!(method.ReturnsVoid || method.ReturnType.Is(KnownType.System_Threading_Tasks_Task) - || method.ReturnType.Is(KnownType.System_Threading_Tasks_Task_T)) + || method.ReturnType.Is(KnownType.System_Threading_Tasks_Task_T))) { - return null; + return $@"contains method ""{method.Name}"" with invalid return type. Only ""void"", ""Task"" and ""Task"" are allowed"; } else { - return $@"contains method ""{method.Name}"" with invalid return type. Only ""void"", ""Task"" and ""Task "" are allowed"; + return null; } } } diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index 6df640bdbee..267fbee0d79 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -67,6 +67,7 @@ internal sealed class KnownType internal static readonly KnownType Microsoft_AspNetCore_Mvc_RequestSizeLimitAttribute = new("Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute"); internal static readonly KnownType Microsoft_AspNetCore_Razor_Hosting_RazorCompiledItemAttribute = new("Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute"); internal static readonly KnownType Microsoft_Azure_WebJobs_Extensions_DurableTask_IDurableEntityClient = new("Microsoft.Azure.WebJobs.Extensions.DurableTask.IDurableEntityClient"); + internal static readonly KnownType Microsoft_Azure_WebJobs_Extensions_DurableTask_IDurableEntityContext = new("Microsoft.Azure.WebJobs.Extensions.DurableTask.IDurableEntityContext"); internal static readonly KnownType Microsoft_Azure_WebJobs_Extensions_DurableTask_IDurableOrchestrationContext = new("Microsoft.Azure.WebJobs.Extensions.DurableTask.IDurableOrchestrationContext"); internal static readonly KnownType Microsoft_Azure_WebJobs_FunctionNameAttribute = new("Microsoft.Azure.WebJobs.FunctionNameAttribute"); internal static readonly KnownType Microsoft_Data_Sqlite_SqliteCommand = new("Microsoft.Data.Sqlite.SqliteCommand"); diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CloudNative/DurableEntityInterfaceRestrictions.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CloudNative/DurableEntityInterfaceRestrictions.cs index 64299e7d6f4..3145fadcf03 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CloudNative/DurableEntityInterfaceRestrictions.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/CloudNative/DurableEntityInterfaceRestrictions.cs @@ -18,6 +18,11 @@ public interface IValid Task TaskStrArg(int count); } +public interface IEmpty +{ + // This is invalid and will throw +} + public interface IInheritsEmptyWithValid : IEmpty { void Valid(); @@ -33,11 +38,6 @@ public interface IInheritsValidIsEmpty2 : IInheritsValidIsEmpty // Do not add anything => still valid - another level of nesting } -public interface IEmpty -{ - // This is invalid and will throw -} - public interface IInheritsEmptyIsEmpty : IEmpty { // This is invalid and will throw @@ -107,16 +107,15 @@ public interface IEvent event EventHandler Event; } -public class UseDurableClient +public class UseDurableEntityClient { - private readonly IDurableClient client; + private readonly IDurableEntityClient client; private readonly EntityId id; public async Task UnrelatedMethods() { - await client.ReadEntityStateAsync (id); - await client.StartNewAsync ("name", null); - await client.SignalEntityAsync(id, "name"); // Always compliant, same name but not generic + await client.ReadEntityStateAsync (id); // T is a type of the returned data. It's not an entity interface. + await client.SignalEntityAsync(id, "name"); // Always compliant, same name but not generic } public async Task Compliant() @@ -152,6 +151,11 @@ public async Task Reasons() await client.SignalEntityAsync (id, x => { }); // Noncompliant {{Use valid entity interface. IIndexer contains property "this[]". Only methods are allowed.}} await client.SignalEntityAsync (id, x => { }); // Noncompliant {{Use valid entity interface. IEvent contains event "Event". Only methods are allowed.}} } + + public async Task FromDurableClient(IDurableClient inheritedClient) + { + await inheritedClient.SignalEntityAsync (id, x => { }); // Noncompliant + } } public class UseDurableOrchestrationContext @@ -179,6 +183,20 @@ public void Errors() } } +public class UseDurableEntityContext +{ + private readonly IDurableEntityContext context; + private readonly EntityId id; + + public void Overloads() + { + context.SignalEntity (id, x => { }); // Noncompliant + context.SignalEntity ("id", x => { }); // Noncompliant + context.SignalEntity (id, DateTime.Now, x => { }); // Noncompliant + context.SignalEntity ("id", DateTime.Now, x => { }); // Noncompliant + } +} + public class AnotherType { public void SignalEntityAsync () { }