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

Attribute Binding Roslyn Analyzers #301

Merged
merged 6 commits into from
May 3, 2024
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
5 changes: 4 additions & 1 deletion src/Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
5 changes: 5 additions & 0 deletions src/Analyzers/AnalyzersCategories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ static class AnalyzersCategories
/// The category for the orchestration related analyzers.
/// </summary>
public const string Orchestration = "Orchestration";

/// <summary>
/// The category for the attribute binding related analyzers.
/// </summary>
public const string AttributeBinding = "Attribute Binding";
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Analyzer that matches 'DurableClientAttribute' with 'DurableTaskClient' parameters.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DurableClientBindingAnalyzer : MatchingAttributeBindingAnalyzer
{
/// <summary>
/// Diagnostic ID supported for the analyzer.
/// </summary>
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);

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

/// <inheritdoc/>
protected override ExpectedBinding GetExpectedBinding(KnownTypeSymbols knownTypeSymbols)
{
return new ExpectedBinding()
{
Attribute = knownTypeSymbols.DurableClientAttribute,
Type = knownTypeSymbols.DurableTaskClient,
};
}

/// <inheritdoc/>
protected override void ReportDiagnostic(SymbolAnalysisContext ctx, ExpectedBinding expected, IParameterSymbol parameter)
{
string wrongType = parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
ctx.ReportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, parameter, wrongType));
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Analyzer that matches 'EntityTriggerAttribute' with 'TaskEntityDispatcher' parameters.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class EntityTriggerBindingAnalyzer : MatchingAttributeBindingAnalyzer
{
/// <summary>
/// Diagnostic ID supported for the analyzer.
/// </summary>
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);

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

/// <inheritdoc/>
protected override ExpectedBinding GetExpectedBinding(KnownTypeSymbols knownTypeSymbols)
{
return new ExpectedBinding()
{
Attribute = knownTypeSymbols.EntityTriggerAttribute,
Type = knownTypeSymbols.TaskEntityDispatcher,
};
}

/// <inheritdoc/>
protected override void ReportDiagnostic(SymbolAnalysisContext ctx, ExpectedBinding expected, IParameterSymbol parameter)
{
string wrongType = parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
ctx.ReportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, parameter, wrongType));
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Expected attribute binding for a given parameter type.
/// </summary>
public struct ExpectedBinding
{
/// <summary>
/// Gets or sets the expected attribute.
/// </summary>
public INamedTypeSymbol? Attribute { get; set; }

/// <summary>
/// Gets or sets the expected type.
/// </summary>
public INamedTypeSymbol? Type { get; set; }
}

/// <summary>
/// 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.
/// </summary>
public abstract class MatchingAttributeBindingAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
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);
});
}

/// <summary>
/// Gets the expected attribute binding and the related type to be used during parameters analysis.
/// </summary>
/// <param name="knownTypeSymbols">The set of well-known types.</param>
/// <returns>The expected binding for this analyzer.</returns>
protected abstract ExpectedBinding GetExpectedBinding(KnownTypeSymbols knownTypeSymbols);

/// <summary>
/// After an incorrect attribute/type matching is found, this method is called so the concrete implementation can report a diagnostic.
/// </summary>
/// <param name="ctx">Context for a symbol action. Allows reporting a diagnostic.</param>
/// <param name="expected">Expected binding for an attribute/type.</param>
/// <param name="parameter">Analyzed parameter symbol.</param>
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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Analyzer that matches 'OrchestrationTriggerAttribute' with 'TaskOrchestrationContext' parameters.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class OrchestrationTriggerBindingAnalyzer : MatchingAttributeBindingAnalyzer
{
/// <summary>
/// Diagnostic ID supported for the analyzer.
/// </summary>
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);

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

/// <inheritdoc/>
protected override ExpectedBinding GetExpectedBinding(KnownTypeSymbols knownTypeSymbols)
{
return new ExpectedBinding()
{
Attribute = knownTypeSymbols.FunctionOrchestrationAttribute,
Type = knownTypeSymbols.TaskOrchestrationContext,
};
}

/// <inheritdoc/>
protected override void ReportDiagnostic(SymbolAnalysisContext ctx, ExpectedBinding expected, IParameterSymbol parameter)
{
string wrongType = parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
ctx.ReportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, parameter, wrongType));
}
}
47 changes: 47 additions & 0 deletions src/Analyzers/KnownTypeSymbols.Durable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.CodeAnalysis;

namespace Microsoft.DurableTask.Analyzers;

/// <summary>
/// Provides a set of well-known types that are used by the analyzers.
/// Inspired by KnownTypeSymbols class in
/// <see href="https://github.com/dotnet/runtime/blob/2a846acb1a92e811427babe3ff3f047f98c5df02/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs">System.Text.Json.SourceGeneration</see> 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.
/// </summary>
public sealed partial class KnownTypeSymbols
{
INamedTypeSymbol? taskOrchestratorInterface;
INamedTypeSymbol? taskOrchestratorBaseClass;
INamedTypeSymbol? durableTaskRegistry;
INamedTypeSymbol? taskOrchestrationContext;
INamedTypeSymbol? durableTaskClient;

/// <summary>
/// Gets an ITaskOrchestrator type symbol.
/// </summary>
public INamedTypeSymbol? TaskOrchestratorInterface => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.ITaskOrchestrator", ref this.taskOrchestratorInterface);

/// <summary>
/// Gets a TaskOrchestrator type symbol.
/// </summary>
public INamedTypeSymbol? TaskOrchestratorBaseClass => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.TaskOrchestrator`2", ref this.taskOrchestratorBaseClass);

/// <summary>
/// Gets a DurableTaskRegistry type symbol.
/// </summary>
public INamedTypeSymbol? DurableTaskRegistry => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.DurableTaskRegistry", ref this.durableTaskRegistry);

/// <summary>
/// Gets a TaskOrchestrationContext type symbol.
/// </summary>
public INamedTypeSymbol? TaskOrchestrationContext => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.TaskOrchestrationContext", ref this.taskOrchestrationContext);

Check warning on line 41 in src/Analyzers/KnownTypeSymbols.Durable.cs

View workflow job for this annotation

GitHub Actions / build

Check warning on line 41 in src/Analyzers/KnownTypeSymbols.Durable.cs

View workflow job for this annotation

GitHub Actions / build


/// <summary>
/// Gets a DurableTaskClient type symbol.
/// </summary>
public INamedTypeSymbol? DurableTaskClient => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.Client.DurableTaskClient", ref this.durableTaskClient);
}
46 changes: 46 additions & 0 deletions src/Analyzers/KnownTypeSymbols.Functions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.CodeAnalysis;

namespace Microsoft.DurableTask.Analyzers;

/// <summary>
/// Provides a set of well-known types that are used by the analyzers.
/// Inspired by KnownTypeSymbols class in
/// <see href="https://github.com/dotnet/runtime/blob/2a846acb1a92e811427babe3ff3f047f98c5df02/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs">System.Text.Json.SourceGeneration</see> 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.
/// </summary>
public sealed partial class KnownTypeSymbols
{
INamedTypeSymbol? functionOrchestrationAttribute;
INamedTypeSymbol? functionNameAttribute;
INamedTypeSymbol? durableClientAttribute;
INamedTypeSymbol? entityTriggerAttribute;
INamedTypeSymbol? taskEntityDispatcher;

/// <summary>
/// Gets an OrchestrationTriggerAttribute type symbol.
/// </summary>
public INamedTypeSymbol? FunctionOrchestrationAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.OrchestrationTriggerAttribute", ref this.functionOrchestrationAttribute);

/// <summary>
/// Gets a FunctionNameAttribute type symbol.
/// </summary>
public INamedTypeSymbol? FunctionNameAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.FunctionAttribute", ref this.functionNameAttribute);

/// <summary>
/// Gets a DurableClientAttribute type symbol.
/// </summary>
public INamedTypeSymbol? DurableClientAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.DurableClientAttribute", ref this.durableClientAttribute);

/// <summary>
/// Gets an EntityTriggerAttribute type symbol.
/// </summary>
public INamedTypeSymbol? EntityTriggerAttribute => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.EntityTriggerAttribute", ref this.entityTriggerAttribute);

/// <summary>
/// Gets a TaskEntityDispatcher type symbol.
/// </summary>
public INamedTypeSymbol? TaskEntityDispatcher => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Functions.Worker.TaskEntityDispatcher", ref this.taskEntityDispatcher);
}
40 changes: 40 additions & 0 deletions src/Analyzers/KnownTypeSymbols.Net.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.CodeAnalysis;

namespace Microsoft.DurableTask.Analyzers;

/// <summary>
/// Provides a set of well-known types that are used by the analyzers.
/// Inspired by KnownTypeSymbols class in
/// <see href="https://github.com/dotnet/runtime/blob/2a846acb1a92e811427babe3ff3f047f98c5df02/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs">System.Text.Json.SourceGeneration</see> 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.
/// </summary>
public sealed partial class KnownTypeSymbols
{
INamedTypeSymbol? guid;
INamedTypeSymbol? thread;
INamedTypeSymbol? task;
INamedTypeSymbol? taskT;

/// <summary>
/// Gets a Guid type symbol.
/// </summary>
public INamedTypeSymbol? GuidType => this.GetOrResolveFullyQualifiedType(typeof(Guid).FullName, ref this.guid);

/// <summary>
/// Gets a Thread type symbol.
/// </summary>
public INamedTypeSymbol? Thread => this.GetOrResolveFullyQualifiedType(typeof(Thread).FullName, ref this.thread);

/// <summary>
/// Gets a Task type symbol.
/// </summary>
public INamedTypeSymbol? Task => this.GetOrResolveFullyQualifiedType(typeof(Task).FullName, ref this.task);

/// <summary>
/// Gets a Task&lt;T&gt; type symbol.
/// </summary>
public INamedTypeSymbol? TaskT => this.GetOrResolveFullyQualifiedType(typeof(Task<>).FullName, ref this.taskT);
}
Loading
Loading