Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Function orchestration bindings analyzer #304

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
allantargino marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
Loading