diff --git a/src/Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/AnalyzerReleases.Unshipped.md index cac23ffb..a0f1d8c6 100644 --- a/src/Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/AnalyzerReleases.Unshipped.md @@ -1,4 +1,4 @@ -; Unshipped analyzer release +; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md ### New Rules @@ -8,3 +8,6 @@ Rule ID | Category | Severity | Notes DURABLE0001 | Orchestration | Warning | DateTimeOrchestrationAnalyzer DURABLE0002 | Orchestration | Warning | GuidOrchestrationAnalyzer DURABLE0003 | Orchestration | Warning | DelayOrchestrationAnalyzer +DURABLE1001 | Attribute Binding | Error | OrchestrationTriggerBindingAnalyzer +DURABLE1002 | Attribute Binding | Error | DurableClientBindingAnalyzer +DURABLE1003 | Attribute Binding | Error | EntityTriggerBindingAnalyzer \ No newline at end of file diff --git a/src/Analyzers/AnalyzersCategories.cs b/src/Analyzers/AnalyzersCategories.cs index 2f2b1a6b..8bdae885 100644 --- a/src/Analyzers/AnalyzersCategories.cs +++ b/src/Analyzers/AnalyzersCategories.cs @@ -12,4 +12,9 @@ static class AnalyzersCategories /// The category for the orchestration related analyzers. /// public const string Orchestration = "Orchestration"; + + /// + /// The category for the attribute binding related analyzers. + /// + public const string AttributeBinding = "Attribute Binding"; } diff --git a/src/Analyzers/Functions/AttributeBinding/DurableClientBindingAnalyzer.cs b/src/Analyzers/Functions/AttributeBinding/DurableClientBindingAnalyzer.cs new file mode 100644 index 00000000..8089043e --- /dev/null +++ b/src/Analyzers/Functions/AttributeBinding/DurableClientBindingAnalyzer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.DurableTask.Analyzers.Functions.AttributeBinding; + +/// +/// Analyzer that matches 'DurableClientAttribute' with 'DurableTaskClient' parameters. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class DurableClientBindingAnalyzer : MatchingAttributeBindingAnalyzer +{ + /// + /// Diagnostic ID supported for the analyzer. + /// + public const string DiagnosticId = "DURABLE1002"; + + static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.DurableClientBindingAnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.DurableClientBindingAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + + static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + AnalyzersCategories.AttributeBinding, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => [Rule]; + + /// + protected override ExpectedBinding GetExpectedBinding(KnownTypeSymbols knownTypeSymbols) + { + return new ExpectedBinding() + { + Attribute = knownTypeSymbols.DurableClientAttribute, + Type = knownTypeSymbols.DurableTaskClient, + }; + } + + /// + protected override void ReportDiagnostic(SymbolAnalysisContext ctx, ExpectedBinding expected, IParameterSymbol parameter) + { + string wrongType = parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); + ctx.ReportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, parameter, wrongType)); + } +} diff --git a/src/Analyzers/Functions/AttributeBinding/EntityTriggerBindingAnalyzer.cs b/src/Analyzers/Functions/AttributeBinding/EntityTriggerBindingAnalyzer.cs new file mode 100644 index 00000000..8667cb32 --- /dev/null +++ b/src/Analyzers/Functions/AttributeBinding/EntityTriggerBindingAnalyzer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.DurableTask.Analyzers.Functions.AttributeBinding; + +/// +/// Analyzer that matches 'EntityTriggerAttribute' with 'TaskEntityDispatcher' parameters. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class EntityTriggerBindingAnalyzer : MatchingAttributeBindingAnalyzer +{ + /// + /// Diagnostic ID supported for the analyzer. + /// + public const string DiagnosticId = "DURABLE1003"; + + static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.EntityTriggerBindingAnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.EntityTriggerBindingAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + + static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + AnalyzersCategories.AttributeBinding, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => [Rule]; + + /// + protected override ExpectedBinding GetExpectedBinding(KnownTypeSymbols knownTypeSymbols) + { + return new ExpectedBinding() + { + Attribute = knownTypeSymbols.EntityTriggerAttribute, + Type = knownTypeSymbols.TaskEntityDispatcher, + }; + } + + /// + protected override void ReportDiagnostic(SymbolAnalysisContext ctx, ExpectedBinding expected, IParameterSymbol parameter) + { + string wrongType = parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); + ctx.ReportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, parameter, wrongType)); + } +} diff --git a/src/Analyzers/Functions/AttributeBinding/MatchingAttributeBindingAnalyzer.cs b/src/Analyzers/Functions/AttributeBinding/MatchingAttributeBindingAnalyzer.cs new file mode 100644 index 00000000..28c2df24 --- /dev/null +++ b/src/Analyzers/Functions/AttributeBinding/MatchingAttributeBindingAnalyzer.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.DurableTask.Analyzers.Functions.AttributeBinding; + +/// +/// Expected attribute binding for a given parameter type. +/// +public struct ExpectedBinding +{ + /// + /// Gets or sets the expected attribute. + /// + public INamedTypeSymbol? Attribute { get; set; } + + /// + /// Gets or sets the expected type. + /// + public INamedTypeSymbol? Type { get; set; } +} + +/// +/// Analyzer that inspects the parameter type of a method to ensure it matches the expected attribute binding. +/// It expects one parameter in the DiagnosticRule message template, so the analyzer can report the wrong type. +/// +public abstract class MatchingAttributeBindingAnalyzer : DiagnosticAnalyzer +{ + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction( + ctx => + { + KnownTypeSymbols knownTypeSymbols = new(ctx.Compilation); + + ExpectedBinding expectedBinding = this.GetExpectedBinding(knownTypeSymbols); + if (expectedBinding.Attribute is null || expectedBinding.Type is null) + { + return; + } + + ctx.RegisterSymbolAction(c => this.Analyze(c, expectedBinding), SymbolKind.Parameter); + }); + } + + /// + /// Gets the expected attribute binding and the related type to be used during parameters analysis. + /// + /// The set of well-known types. + /// The expected binding for this analyzer. + protected abstract ExpectedBinding GetExpectedBinding(KnownTypeSymbols knownTypeSymbols); + + /// + /// After an incorrect attribute/type matching is found, this method is called so the concrete implementation can report a diagnostic. + /// + /// Context for a symbol action. Allows reporting a diagnostic. + /// Expected binding for an attribute/type. + /// Analyzed parameter symbol. + protected abstract void ReportDiagnostic(SymbolAnalysisContext ctx, ExpectedBinding expected, IParameterSymbol parameter); + + void Analyze(SymbolAnalysisContext ctx, ExpectedBinding expected) + { + IParameterSymbol parameter = (IParameterSymbol)ctx.Symbol; + + if (parameter.GetAttributes().Any(a => expected.Attribute!.Equals(a.AttributeClass, SymbolEqualityComparer.Default))) + { + if (!parameter.Type.Equals(expected.Type, SymbolEqualityComparer.Default)) + { + this.ReportDiagnostic(ctx, expected, parameter); + } + } + } +} diff --git a/src/Analyzers/Functions/AttributeBinding/OrchestrationTriggerBindingAnalyzer.cs b/src/Analyzers/Functions/AttributeBinding/OrchestrationTriggerBindingAnalyzer.cs new file mode 100644 index 00000000..d6642043 --- /dev/null +++ b/src/Analyzers/Functions/AttributeBinding/OrchestrationTriggerBindingAnalyzer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.DurableTask.Analyzers.Functions.AttributeBinding; + +/// +/// Analyzer that matches 'OrchestrationTriggerAttribute' with 'TaskOrchestrationContext' parameters. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class OrchestrationTriggerBindingAnalyzer : MatchingAttributeBindingAnalyzer +{ + /// + /// Diagnostic ID supported for the analyzer. + /// + public const string DiagnosticId = "DURABLE1001"; + + static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.OrchestrationTriggerBindingAnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.OrchestrationTriggerBindingAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + + static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + AnalyzersCategories.AttributeBinding, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => [Rule]; + + /// + protected override ExpectedBinding GetExpectedBinding(KnownTypeSymbols knownTypeSymbols) + { + return new ExpectedBinding() + { + Attribute = knownTypeSymbols.FunctionOrchestrationAttribute, + Type = knownTypeSymbols.TaskOrchestrationContext, + }; + } + + /// + protected override void ReportDiagnostic(SymbolAnalysisContext ctx, ExpectedBinding expected, IParameterSymbol parameter) + { + string wrongType = parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); + ctx.ReportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, parameter, wrongType)); + } +} diff --git a/src/Analyzers/KnownTypeSymbols.Durable.cs b/src/Analyzers/KnownTypeSymbols.Durable.cs new file mode 100644 index 00000000..d314f564 --- /dev/null +++ b/src/Analyzers/KnownTypeSymbols.Durable.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.DurableTask.Analyzers; + +/// +/// Provides a set of well-known types that are used by the analyzers. +/// Inspired by KnownTypeSymbols class in +/// System.Text.Json.SourceGeneration source code. +/// Lazy initialization is used to avoid the the initialization of all types during class construction, since not all symbols are used by all analyzers. +/// +public sealed partial class KnownTypeSymbols +{ + INamedTypeSymbol? taskOrchestratorInterface; + INamedTypeSymbol? taskOrchestratorBaseClass; + INamedTypeSymbol? durableTaskRegistry; + INamedTypeSymbol? taskOrchestrationContext; + INamedTypeSymbol? durableTaskClient; + + /// + /// Gets an ITaskOrchestrator type symbol. + /// + public INamedTypeSymbol? TaskOrchestratorInterface => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.ITaskOrchestrator", ref this.taskOrchestratorInterface); + + /// + /// Gets a TaskOrchestrator type symbol. + /// + public INamedTypeSymbol? TaskOrchestratorBaseClass => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.TaskOrchestrator`2", ref this.taskOrchestratorBaseClass); + + /// + /// Gets a DurableTaskRegistry type symbol. + /// + public INamedTypeSymbol? DurableTaskRegistry => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.DurableTaskRegistry", ref this.durableTaskRegistry); + + /// + /// Gets a TaskOrchestrationContext type symbol. + /// + public INamedTypeSymbol? TaskOrchestrationContext => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.TaskOrchestrationContext", ref this.taskOrchestrationContext); + + + /// + /// Gets a DurableTaskClient type symbol. + /// + public INamedTypeSymbol? DurableTaskClient => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.Client.DurableTaskClient", ref this.durableTaskClient); +} diff --git a/src/Analyzers/KnownTypeSymbols.Functions.cs b/src/Analyzers/KnownTypeSymbols.Functions.cs new file mode 100644 index 00000000..a0c106ba --- /dev/null +++ b/src/Analyzers/KnownTypeSymbols.Functions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.DurableTask.Analyzers; + +/// +/// Provides a set of well-known types that are used by the analyzers. +/// Inspired by KnownTypeSymbols class in +/// System.Text.Json.SourceGeneration source code. +/// Lazy initialization is used to avoid the the initialization of all types during class construction, since not all symbols are used by all analyzers. +/// +public sealed partial class KnownTypeSymbols +{ + INamedTypeSymbol? functionOrchestrationAttribute; + INamedTypeSymbol? functionNameAttribute; + INamedTypeSymbol? durableClientAttribute; + INamedTypeSymbol? entityTriggerAttribute; + INamedTypeSymbol? taskEntityDispatcher; + + /// + /// Gets an OrchestrationTriggerAttribute type symbol. + /// + public INamedTypeSymbol? FunctionOrchestrationAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.OrchestrationTriggerAttribute", ref this.functionOrchestrationAttribute); + + /// + /// Gets a FunctionNameAttribute type symbol. + /// + public INamedTypeSymbol? FunctionNameAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.FunctionAttribute", ref this.functionNameAttribute); + + /// + /// Gets a DurableClientAttribute type symbol. + /// + public INamedTypeSymbol? DurableClientAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.DurableClientAttribute", ref this.durableClientAttribute); + + /// + /// Gets an EntityTriggerAttribute type symbol. + /// + public INamedTypeSymbol? EntityTriggerAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.EntityTriggerAttribute", ref this.entityTriggerAttribute); + + /// + /// Gets a TaskEntityDispatcher type symbol. + /// + public INamedTypeSymbol? TaskEntityDispatcher => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.TaskEntityDispatcher", ref this.taskEntityDispatcher); +} diff --git a/src/Analyzers/KnownTypeSymbols.Net.cs b/src/Analyzers/KnownTypeSymbols.Net.cs new file mode 100644 index 00000000..5e7467a7 --- /dev/null +++ b/src/Analyzers/KnownTypeSymbols.Net.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.DurableTask.Analyzers; + +/// +/// Provides a set of well-known types that are used by the analyzers. +/// Inspired by KnownTypeSymbols class in +/// System.Text.Json.SourceGeneration source code. +/// Lazy initialization is used to avoid the the initialization of all types during class construction, since not all symbols are used by all analyzers. +/// +public sealed partial class KnownTypeSymbols +{ + INamedTypeSymbol? guid; + INamedTypeSymbol? thread; + INamedTypeSymbol? task; + INamedTypeSymbol? taskT; + + /// + /// Gets a Guid type symbol. + /// + public INamedTypeSymbol? GuidType => this.GetOrResolveFullyQualifiedType(typeof(Guid).FullName, ref this.guid); + + /// + /// Gets a Thread type symbol. + /// + public INamedTypeSymbol? Thread => this.GetOrResolveFullyQualifiedType(typeof(Thread).FullName, ref this.thread); + + /// + /// Gets a Task type symbol. + /// + public INamedTypeSymbol? Task => this.GetOrResolveFullyQualifiedType(typeof(Task).FullName, ref this.task); + + /// + /// Gets a Task<T> type symbol. + /// + public INamedTypeSymbol? TaskT => this.GetOrResolveFullyQualifiedType(typeof(Task<>).FullName, ref this.taskT); +} diff --git a/src/Analyzers/KnownTypeSymbols.cs b/src/Analyzers/KnownTypeSymbols.cs index 109bd440..a4144fd2 100644 --- a/src/Analyzers/KnownTypeSymbols.cs +++ b/src/Analyzers/KnownTypeSymbols.cs @@ -11,65 +11,10 @@ namespace Microsoft.DurableTask.Analyzers; /// System.Text.Json.SourceGeneration source code. /// Lazy initialization is used to avoid the the initialization of all types during class construction, since not all symbols are used by all analyzers. /// -public sealed class KnownTypeSymbols(Compilation compilation) +public sealed partial class KnownTypeSymbols(Compilation compilation) { readonly Compilation compilation = compilation; - INamedTypeSymbol? functionOrchestrationAttribute; - INamedTypeSymbol? functionNameAttribute; - INamedTypeSymbol? taskOrchestratorInterface; - INamedTypeSymbol? taskOrchestratorBaseClass; - INamedTypeSymbol? durableTaskRegistry; - INamedTypeSymbol? guid; - INamedTypeSymbol? thread; - INamedTypeSymbol? task; - INamedTypeSymbol? taskT; - - /// - /// Gets an OrchestrationTriggerAttribute type symbol. - /// - public INamedTypeSymbol? FunctionOrchestrationAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.OrchestrationTriggerAttribute", ref this.functionOrchestrationAttribute); - - /// - /// Gets a FunctionNameAttribute type symbol. - /// - public INamedTypeSymbol? FunctionNameAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.FunctionAttribute", ref this.functionNameAttribute); - - /// - /// Gets an ITaskOrchestrator type symbol. - /// - public INamedTypeSymbol? TaskOrchestratorInterface => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.ITaskOrchestrator", ref this.taskOrchestratorInterface); - - /// - /// Gets a TaskOrchestrator type symbol. - /// - public INamedTypeSymbol? TaskOrchestratorBaseClass => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.TaskOrchestrator`2", ref this.taskOrchestratorBaseClass); - - /// - /// Gets a DurableTaskRegistry type symbol. - /// - public INamedTypeSymbol? DurableTaskRegistry => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.DurableTaskRegistry", ref this.durableTaskRegistry); - - /// - /// Gets a Guid type symbol. - /// - public INamedTypeSymbol? GuidType => this.GetOrResolveFullyQualifiedType(typeof(Guid).FullName, ref this.guid); - - /// - /// Gets a Thread type symbol. - /// - public INamedTypeSymbol? Thread => this.GetOrResolveFullyQualifiedType(typeof(Thread).FullName, ref this.thread); - - /// - /// Gets a Task type symbol. - /// - public INamedTypeSymbol? Task => this.GetOrResolveFullyQualifiedType(typeof(Task).FullName, ref this.task); - - /// - /// Gets a Task<T> type symbol. - /// - public INamedTypeSymbol? TaskT => this.GetOrResolveFullyQualifiedType(typeof(Task<>).FullName, ref this.taskT); - INamedTypeSymbol? GetOrResolveFullyQualifiedType(string fullyQualifiedName, ref INamedTypeSymbol? field) { if (field != null) diff --git a/src/Analyzers/Resources.resx b/src/Analyzers/Resources.resx index 96e7ca8b..d15e4b9b 100644 --- a/src/Analyzers/Resources.resx +++ b/src/Analyzers/Resources.resx @@ -135,4 +135,22 @@ Thread.Sleep and Task.Delay calls are not allowed inside an orchestrator + + [OrchestrationTrigger] is associated with the wrong type '{0}', it must be used with TaskOrchestrationContext + + + [OrchestrationTrigger] must be used with TaskOrchestrationContext + + + [EntityTrigger] is associated with the wrong type '{0}', it must be used with TaskEntityDispatcher + + + [EntityTrigger] must be used with TaskOrchestrationContext + + + [DurableClient] is associated with the wrong type '{0}', it must be used with DurableTaskClient + + + [DurableClient] must be used with DurableTaskClient + \ No newline at end of file diff --git a/test/Analyzers.Tests/Functions/AttributeBinding/DurableClientBindingAnalyzerTests.cs b/test/Analyzers.Tests/Functions/AttributeBinding/DurableClientBindingAnalyzerTests.cs new file mode 100644 index 00000000..a44842fb --- /dev/null +++ b/test/Analyzers.Tests/Functions/AttributeBinding/DurableClientBindingAnalyzerTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Analyzers.Functions.AttributeBinding; + +namespace Microsoft.DurableTask.Analyzers.Tests.Functions.AttributeBinding; + +public class DurableClientBindingAnalyzerTests : MatchingAttributeBindingSpecificationTests +{ + protected override string ExpectedDiagnosticId => DurableClientBindingAnalyzer.DiagnosticId; + + protected override string ExpectedAttribute => "[DurableClient]"; + + protected override string ExpectedType => "DurableTaskClient"; +} diff --git a/test/Analyzers.Tests/Functions/AttributeBinding/EntityTriggerBindingAnalyzerTests.cs b/test/Analyzers.Tests/Functions/AttributeBinding/EntityTriggerBindingAnalyzerTests.cs new file mode 100644 index 00000000..bde399f4 --- /dev/null +++ b/test/Analyzers.Tests/Functions/AttributeBinding/EntityTriggerBindingAnalyzerTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Analyzers.Functions.AttributeBinding; + +namespace Microsoft.DurableTask.Analyzers.Tests.Functions.AttributeBinding; + +public class EntityTriggerBindingAnalyzerTests : MatchingAttributeBindingSpecificationTests +{ + protected override string ExpectedDiagnosticId => EntityTriggerBindingAnalyzer.DiagnosticId; + + protected override string ExpectedAttribute => "[EntityTrigger]"; + + protected override string ExpectedType => "TaskEntityDispatcher"; +} diff --git a/test/Analyzers.Tests/Functions/AttributeBinding/MatchingAttributeBindingSpecificationTests.cs b/test/Analyzers.Tests/Functions/AttributeBinding/MatchingAttributeBindingSpecificationTests.cs new file mode 100644 index 00000000..35018340 --- /dev/null +++ b/test/Analyzers.Tests/Functions/AttributeBinding/MatchingAttributeBindingSpecificationTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis.Testing; +using Microsoft.DurableTask.Analyzers.Functions.AttributeBinding; +using Microsoft.DurableTask.Analyzers.Tests.Verifiers; + +namespace Microsoft.DurableTask.Analyzers.Tests.Functions.AttributeBinding; + +public abstract class MatchingAttributeBindingSpecificationTests where TAnalyzer : MatchingAttributeBindingAnalyzer, new() +{ + protected abstract string ExpectedDiagnosticId { get; } + protected abstract string ExpectedAttribute { get; } + protected abstract string ExpectedType { get; } + protected virtual string WrongType { get; } = "int"; + + [Fact] + public async Task EmptyCodeHasNoDiag() + { + string code = @""; + + await VerifyAsync(code); + } + + [Fact] + public async Task TypeWithoutExpectedAttributeHasNoDiag() + { + string code = Wrapper.WrapDurableFunctionOrchestration($@" +void Method({this.ExpectedType} paramName) +{{ +}} +"); + + await VerifyAsync(code); + } + + [Fact] + public async Task ExpectedAttributeWithExpectedTypeHasNoDiag() + { + string code = Wrapper.WrapDurableFunctionOrchestration($@" +void Method({this.ExpectedAttribute} {this.ExpectedType} paramName) +{{ +}} +"); + + await VerifyAsync(code); + } + + [Fact] + public async Task ExpectedAttributeWithWrongTypeHasDiag() + { + string code = Wrapper.WrapDurableFunctionOrchestration($@" +void Method({{|#0:{this.ExpectedAttribute} {this.WrongType} paramName|}}) +{{ +}} +"); + + DiagnosticResult expected = this.BuildDiagnostic().WithLocation(0).WithArguments(this.WrongType); + + await VerifyAsync(code, expected); + } + + static async Task VerifyAsync(string source, params DiagnosticResult[] expected) + { + await CSharpAnalyzerVerifier.VerifyDurableTaskAnalyzerAsync(source, expected); + } + + DiagnosticResult BuildDiagnostic() + { + return CSharpAnalyzerVerifier.Diagnostic(this.ExpectedDiagnosticId); + } +} diff --git a/test/Analyzers.Tests/Functions/AttributeBinding/OrchestrationTriggerBindingAnalyzerTests.cs b/test/Analyzers.Tests/Functions/AttributeBinding/OrchestrationTriggerBindingAnalyzerTests.cs new file mode 100644 index 00000000..f1d020f9 --- /dev/null +++ b/test/Analyzers.Tests/Functions/AttributeBinding/OrchestrationTriggerBindingAnalyzerTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Analyzers.Functions.AttributeBinding; + +namespace Microsoft.DurableTask.Analyzers.Tests.Functions.AttributeBinding; + +public class OrchestrationTriggerBindingAnalyzerTests : MatchingAttributeBindingSpecificationTests +{ + protected override string ExpectedDiagnosticId => OrchestrationTriggerBindingAnalyzer.DiagnosticId; + + protected override string ExpectedAttribute => "[OrchestrationTrigger]"; + + protected override string ExpectedType => "TaskOrchestrationContext"; +}