Skip to content

Commit

Permalink
Function orchestration bindings analyzer (#304)
Browse files Browse the repository at this point in the history
Adds an analyzer that reports a warning when a Durable Function orchestration has parameters bindings other than `OrchestrationTrigger` (such as `DurableClient` or `EntityTrigger`).
  • Loading branch information
allantargino authored May 6, 2024
1 parent c4fac92 commit c65608e
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ DURABLE0001 | Orchestration | Warning | DateTimeOrchestrationAnalyzer
DURABLE0002 | Orchestration | Warning | GuidOrchestrationAnalyzer
DURABLE0003 | Orchestration | Warning | DelayOrchestrationAnalyzer
DURABLE0007 | Orchestration | Warning | CancellationTokenOrchestrationAnalyzer
DURABLE0008 | Orchestration | Warning | OtherBindingsOrchestrationAnalyzer
DURABLE1001 | Attribute Binding | Error | OrchestrationTriggerBindingAnalyzer
DURABLE1002 | Attribute Binding | Error | DurableClientBindingAnalyzer
DURABLE1003 | Attribute Binding | Error | EntityTriggerBindingAnalyzer
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.DurableTask.Analyzers.Orchestration;
using static Microsoft.DurableTask.Analyzers.Functions.Orchestration.OtherBindingsOrchestrationAnalyzer;

namespace Microsoft.DurableTask.Analyzers.Functions.Orchestration;

/// <summary>
/// Analyzer that reports a warning when a Durable Function Orchestration has parameters bindings other than OrchestrationTrigger.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
class OtherBindingsOrchestrationAnalyzer : OrchestrationAnalyzer<OtherBindingsOrchestrationOrchestrationVisitor>
{
/// <summary>
/// Diagnostic ID supported for the analyzer.
/// </summary>
public const string DiagnosticId = "DURABLE0008";

static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.OtherBindingsOrchestrationAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.OtherBindingsOrchestrationAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));

static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
Title,
MessageFormat,
AnalyzersCategories.Orchestration,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];

/// <summary>
/// Visitor that inspects Durable Functions' method signatures for parameters binding other than OrchestrationTrigger.
/// </summary>
public sealed class OtherBindingsOrchestrationOrchestrationVisitor : OrchestrationVisitor
{
ImmutableArray<INamedTypeSymbol> bannedBindings;

/// <inheritdoc/>
public override bool Initialize()
{
List<INamedTypeSymbol?> candidateSymbols = [
this.KnownTypeSymbols.DurableClientAttribute,
this.KnownTypeSymbols.EntityTriggerAttribute,
];

// filter out null values, since some of them may not be available during compilation
this.bannedBindings = candidateSymbols.Where(s => s != null).ToImmutableArray()!;

return this.bannedBindings.Length > 0;
}

/// <inheritdoc/>
public override void VisitDurableFunction(SemanticModel sm, MethodDeclarationSyntax methodSyntax, IMethodSymbol methodSymbol, string orchestrationName, Action<Diagnostic> reportDiagnostic)
{
foreach (IParameterSymbol parameter in methodSymbol.Parameters)
{
IEnumerable<INamedTypeSymbol?> attributesSymbols = parameter.GetAttributes().Select(att => att.AttributeClass);

if (attributesSymbols.Any(att => att != null && this.bannedBindings.Contains(att, SymbolEqualityComparer.Default)))
{
reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, parameter, orchestrationName));
}
}
}
}
}
6 changes: 6 additions & 0 deletions src/Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,10 @@
<data name="CancellationTokenOrchestrationAnalyzerTitle" xml:space="preserve">
<value>CancellationToken should not be used as an orchestrator function parameter</value>
</data>
<data name="OtherBindingsOrchestrationAnalyzerMessageFormat" xml:space="preserve">
<value>Orchestration '{0}' is using multiple bindings</value>
</data>
<data name="OtherBindingsOrchestrationAnalyzerTitle" xml:space="preserve">
<value>OrchestrationTrigger methods must not use any other bindings</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.CodeAnalysis.Testing;
using Microsoft.DurableTask.Analyzers.Functions.Orchestration;
using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier<Microsoft.DurableTask.Analyzers.Functions.Orchestration.OtherBindingsOrchestrationAnalyzer>;

namespace Microsoft.DurableTask.Analyzers.Tests.Functions.Orchestration;

public class OtherBindingsOrchestrationAnalyzerTests
{
[Fact]
public async Task EmptyCodeHasNoDiag()
{
string code = @"";

await VerifyCS.VerifyDurableTaskAnalyzerAsync(code);
}

[Fact]
public async Task DurableFunctionOrchestrationWithNoBannedBindingHasNoDiag()
{
string code = Wrapper.WrapDurableFunctionOrchestration(@"
[Function(""Run"")]
void Method([OrchestrationTrigger] TaskOrchestrationContext context)
{
}
");

await VerifyCS.VerifyDurableTaskAnalyzerAsync(code);
}

[Fact]
public async Task DurableFunctionOrchestrationUsingDurableClientHasDiag()
{
string code = Wrapper.WrapDurableFunctionOrchestration(@"
[Function(""Run"")]
void Method([OrchestrationTrigger] TaskOrchestrationContext context, {|#0:[DurableClient] DurableTaskClient client|})
{
}
");

DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run");

await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected);
}

[Fact]
public async Task DurableFunctionOrchestrationUsingEntityTriggerHasDiag()
{
string code = Wrapper.WrapDurableFunctionOrchestration(@"
[Function(""Run"")]
void Method([OrchestrationTrigger] TaskOrchestrationContext context, {|#0:[EntityTrigger] TaskEntityDispatcher dispatcher|})
{
}
");

DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run");

await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected);
}


[Fact]
public async Task DurableFunctionOrchestrationUsingMultipleBannedBindingsHasDiag()
{
string code = Wrapper.WrapDurableFunctionOrchestration(@"
[Function(""Run"")]
void Method([OrchestrationTrigger] TaskOrchestrationContext context,
{|#0:[EntityTrigger] TaskEntityDispatcher dispatcher|},
{|#1:[DurableClient] DurableTaskClient client|})
{
}
");

DiagnosticResult[] expected = Enumerable.Range(0, 2).Select(
i => BuildDiagnostic().WithLocation(i).WithArguments("Run")).ToArray();

await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected);
}

static DiagnosticResult BuildDiagnostic()
{
return VerifyCS.Diagnostic(OtherBindingsOrchestrationAnalyzer.DiagnosticId);
}
}

0 comments on commit c65608e

Please sign in to comment.