Skip to content

Commit

Permalink
Rule S6424: Azure Functions - Entity interfaces restrictions (#5681)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavel-mikula-sonarsource authored Jun 3, 2022
1 parent a2e110d commit 910af4d
Show file tree
Hide file tree
Showing 10 changed files with 509 additions and 2 deletions.
85 changes: 85 additions & 0 deletions analyzers/rspec/cs/S6424_c#.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<p>The recommended way to access Azure Durable Entities is Interfaces via generated proxy objects.</p>
<p>The following restrictions, during interface design, are enforced:</p>
<ul>
<li> Entity interfaces must be defined in the same assembly as the entity class. This is not detected by the rule. </li>
<li> Entity interfaces must only define methods. </li>
<li> Entity interfaces must not contain generic parameters. </li>
<li> Entity interface methods must not have more than one parameter. </li>
<li> Entity interface methods must return void, Task, or Task&lt;T&gt;. </li>
</ul>
<p>If any of these rules are violated, an <code>InvalidOperationException</code> is thrown at runtime when the interface is used as a type argument to
<code>IDurableEntityContext.SignalEntity&lt;TEntityInterface&gt;</code>, <code>IDurableEntityClient.SignalEntityAsync&lt;TEntityInterface&gt;</code>
or <code>IDurableOrchestrationContext.CreateEntityProxy&lt;TEntityInterface&gt;</code>. The exception message explains which rule was broken.</p>
<p>This rule raises an issue in case any of the restrictions above is not respected.</p>
<h2>Noncompliant Code Example</h2>
<pre>
namespace Foo // Noncompliant, must be defined in the same assembly as the entity class that implements it
{
public interface ICounter&lt;T&gt; // Noncompliant, interfaces cannot contain generic parameters
{
string Name { get; set; } // Noncompliant, interface must only define methods
void Add(int amount, int secondParameter); // Noncompliant, methods must not have more than one parameter
int Get(); // Noncompliant, methods must return void, Task, or Task&lt;T&gt;
}
}

namespace Bar
{
public class Counter : ICounter
{
// do stuff
}

public static class AddToCounterFromQueue
{
[FunctionName("AddToCounterFromQueue")]
public static Task Run(
[QueueTrigger("durable-function-trigger")] string input,
[DurableClient] IDurableEntityClient client)
{
var entityId = new EntityId("Counter", "myCounter");
int amount = int.Parse(input);
return client.SignalEntityAsync&lt;ICounter&gt;(entityId, proxy =&gt; proxy.Add(amount, 10));
}
}
}
</pre>
<h2>Compliant Solution</h2>
<pre>
namespace Bar
{
public interface ICounter
{
void Add(int amount);
Task&lt;int&gt; Get();
}
}

namespace Bar
{
public class Counter : ICounter
{
// do stuff
}

public static class AddToCounterFromQueue
{
[FunctionName("AddToCounterFromQueue")]
public static Task Run(
[QueueTrigger("durable-function-trigger")] string input,
[DurableClient] IDurableEntityClient client)
{
var entityId = new EntityId("Counter", "myCounter");
int amount = int.Parse(input);
return client.SignalEntityAsync&lt;ICounter&gt;(entityId, proxy =&gt; proxy.Add(amount));
}
}
}
</pre>
<h2>See</h2>
<ul>
<li> <a
href="https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-dotnet-entities#restrictions-on-entity-interfaces">Restrictions on Entity Interfaces</a> </li>
<li> <a href="https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-entities?tabs=csharp">Durable Entities</a> </li>
</ul>

17 changes: 17 additions & 0 deletions analyzers/rspec/cs/S6424_c#.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"title": "Azure Functions: Restrictions on entity interfaces.",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "30min"
},
"tags": [
"design"
],
"defaultSeverity": "Blocker",
"ruleSpecification": "RSPEC-6424",
"sqKey": "S6424",
"scope": "Main",
"quickfix": "infeasible"
}
3 changes: 2 additions & 1 deletion analyzers/rspec/cs/Sonar_way_profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@
"S5753",
"S5766",
"S5773",
"S6419"
"S6419",
"S6424"
]
}
33 changes: 33 additions & 0 deletions analyzers/src/SonarAnalyzer.CSharp/RspecStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -12943,6 +12943,39 @@
<data name="S6423_Type" xml:space="preserve">
<value>CODE_SMELL</value>
</data>
<data name="S6424_Category" xml:space="preserve">
<value>Blocker Code Smell</value>
</data>
<data name="S6424_Description" xml:space="preserve">
<value>The recommended way to access Azure Durable Entities is Interfaces via generated proxy objects.</value>
</data>
<data name="S6424_IsActivatedByDefault" xml:space="preserve">
<value>True</value>
</data>
<data name="S6424_Remediation" xml:space="preserve">
<value>Constant/Issue</value>
</data>
<data name="S6424_RemediationCost" xml:space="preserve">
<value>30min</value>
</data>
<data name="S6424_Scope" xml:space="preserve">
<value>Main</value>
</data>
<data name="S6424_Severity" xml:space="preserve">
<value>Blocker</value>
</data>
<data name="S6424_Status" xml:space="preserve">
<value>ready</value>
</data>
<data name="S6424_Tags" xml:space="preserve">
<value>design</value>
</data>
<data name="S6424_Title" xml:space="preserve">
<value>Azure Functions: Restrictions on entity interfaces.</value>
</data>
<data name="S6424_Type" xml:space="preserve">
<value>CODE_SMELL</value>
</data>
<data name="S818_Category" xml:space="preserve">
<value>Minor Code Smell</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2022 SonarSource SA
* mailto: contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using SonarAnalyzer.Helpers;

namespace SonarAnalyzer.Rules
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
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";

private static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorBuilder.GetDescriptor(DiagnosticId, MessageFormat, RspecStrings.ResourceManager);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterSyntaxNodeActionInNonGenerated(c =>
{
var name = (GenericNameSyntax)c.Node;
if (name.Identifier.ValueText is SignalEntityName or SignalEntityAsyncName or CreateEntityProxyName
&& name.TypeArgumentList.Arguments.Count == 1
&& c.SemanticModel.GetSymbolInfo(name).Symbol is IMethodSymbol method
&& IsRestrictedMethod(method)
&& method.TypeArguments.Single() is INamedTypeSymbol { TypeKind: not TypeKind.Error } entityInterface
&& InterfaceErrorMessage(entityInterface) is { } message)
{
c.ReportIssue(Diagnostic.Create(Rule, name.GetLocation(), entityInterface.Name, message));
}
},
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.TypeKind != TypeKind.Interface)
{
return "is not an interface";
}
else if (entityInterface.IsGenericType)
{
return "is generic";
}
else
{
var members = new[] { entityInterface }.Concat(entityInterface.AllInterfaces).SelectMany(x => x.GetMembers()).ToArray();
return members.Any()
? members.Select(MemberErrorMessage).WhereNotNull().FirstOrDefault()
: "is empty";
}
}

private static string MemberErrorMessage(ISymbol member)
{
if (member is not IMethodSymbol method)
{
return $@"contains {member.Kind.ToString().ToLower()} ""{member.Name}"". Only methods are allowed";
}
else if (method.IsGenericMethod)
{
return $@"contains generic method ""{method.Name}""";
}
else if (method.Parameters.Length > 1)
{
return $@"contains method ""{method.Name}"" with {method.Parameters.Length} parameters. Zero or one are allowed";
}
else if (!(method.ReturnsVoid
|| method.ReturnType.Is(KnownType.System_Threading_Tasks_Task)
|| method.ReturnType.Is(KnownType.System_Threading_Tasks_Task_T)))
{
return $@"contains method ""{method.Name}"" with invalid return type. Only ""void"", ""Task"" and ""Task<T>"" are allowed";
}
else
{
return null;
}
}
}
}
3 changes: 3 additions & 0 deletions analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ internal sealed class KnownType
internal static readonly KnownType Microsoft_AspNetCore_Mvc_RequestFormLimitsAttribute = new("Microsoft.AspNetCore.Mvc.RequestFormLimitsAttribute");
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");
internal static readonly KnownType Microsoft_EntityFrameworkCore_DbContextOptionsBuilder = new("Microsoft.EntityFrameworkCore.DbContextOptionsBuilder");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace SonarAnalyzer.UnitTest.MetadataReferences
{
internal static class NuGetMetadataReference
{
#pragma warning disable S103 // Lines should not be too long
// Hardcoded version
public static References MicrosoftVisualStudioQualityToolsUnitTestFramework =>
Create("VS.QualityTools.UnitTestFramework", "15.0.27323.2", null, "Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll");
Expand Down Expand Up @@ -61,6 +62,7 @@ internal static class NuGetMetadataReference
public static References MicrosoftAspNetWebApiCors(string packageVersion) => Create("Microsoft.AspNet.WebApi.Cors", packageVersion);
public static References MicrosoftAzureWebJobs(string packageVersion = Constants.NuGetLatestVersion) => Create("Microsoft.Azure.WebJobs", packageVersion);
public static References MicrosoftAzureWebJobsCore(string packageVersion = Constants.NuGetLatestVersion) => Create("Microsoft.Azure.WebJobs.Core", packageVersion);
public static References MicrosoftAzureWebJobsExtensionsDurableTask(string packageVersion = Constants.NuGetLatestVersion) => Create("Microsoft.Azure.WebJobs.Extensions.DurableTask", packageVersion);
public static References MicrosoftAzureWebJobsExtensionsHttp(string packageVersion = Constants.NuGetLatestVersion) => Create("Microsoft.Azure.WebJobs.Extensions.Http", packageVersion);
public static References MicrosoftBuildNoTargets(string packageVersion = "3.1.0") => Create("Microsoft.Build.NoTargets", packageVersion);
public static References MicrosoftDataSqliteCore(string packageVersion = "2.0.0") => Create("Microsoft.Data.Sqlite.Core", packageVersion);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6348,7 +6348,7 @@ internal static class RuleTypeMappingCS
["S6421"] = "CODE_SMELL",
// ["S6422"],
["S6423"] = "CODE_SMELL",
// ["S6424"],
["S6424"] = "CODE_SMELL",
// ["S6425"],
// ["S6426"],
// ["S6427"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2022 SonarSource SA
* mailto: contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using SonarAnalyzer.Rules;

namespace SonarAnalyzer.UnitTest.Rules
{
[TestClass]
public class DurableEntityInterfaceRestrictionsTest
{
private readonly VerifierBuilder builder = new VerifierBuilder<DurableEntityInterfaceRestrictions>()
.WithBasePath("CloudNative")
.AddReferences(NuGetMetadataReference.MicrosoftAzureWebJobsExtensionsDurableTask());

[TestMethod]
public void DurableEntityInterfaceRestrictions_CS() =>
builder.AddPaths("DurableEntityInterfaceRestrictions.cs").Verify();
}
}
Loading

0 comments on commit 910af4d

Please sign in to comment.