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 @@
  • Entity interface methods must return void, Task, or Task<T>.
  • 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.

    Noncompliant Code Example

    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() { }