-
Notifications
You must be signed in to change notification settings - Fork 231
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
1 parent
a2e110d
commit 910af4d
Showing
10 changed files
with
509 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<T>. </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<TEntityInterface></code>, <code>IDurableEntityClient.SignalEntityAsync<TEntityInterface></code> | ||
or <code>IDurableOrchestrationContext.CreateEntityProxy<TEntityInterface></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<T> // 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<T> | ||
} | ||
} | ||
|
||
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<ICounter>(entityId, proxy => proxy.Add(amount, 10)); | ||
} | ||
} | ||
} | ||
</pre> | ||
<h2>Compliant Solution</h2> | ||
<pre> | ||
namespace Bar | ||
{ | ||
public interface ICounter | ||
{ | ||
void Add(int amount); | ||
Task<int> 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<ICounter>(entityId, proxy => 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> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -257,6 +257,7 @@ | |
"S5753", | ||
"S5766", | ||
"S5773", | ||
"S6419" | ||
"S6419", | ||
"S6424" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
analyzers/src/SonarAnalyzer.CSharp/Rules/CloudNative/DurableEntityInterfaceRestrictions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
.../tests/SonarAnalyzer.UnitTest/Rules/CloudNative/DurableEntityInterfaceRestrictionsTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Oops, something went wrong.