Skip to content

Commit

Permalink
Non-deterministic System.DateTime usage Roslyn Analyzer (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
allantargino authored Apr 18, 2024
1 parent d9438aa commit a275358
Show file tree
Hide file tree
Showing 17 changed files with 1,095 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Microsoft.DurableTask.sln
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "test\Benchmar
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers", "src\Analyzers\Analyzers.csproj", "{998E9D97-BD36-4A9D-81FC-5DAC1CE40083}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Test", "test\Analyzers.Test\Analyzers.Test.csproj", "{541FCCCE-1059-4691-B027-F761CD80DE92}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Analyzers.Tests\Analyzers.Tests.csproj", "{541FCCCE-1059-4691-B027-F761CD80DE92}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
3 changes: 3 additions & 0 deletions src/Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

8 changes: 8 additions & 0 deletions src/Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
DURABLE0001 | Orchestration | Warning | DateTimeOrchestrationAnalyzer
2 changes: 2 additions & 0 deletions src/Analyzers/Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynComponent>true</IsRoslynComponent>
<EnableStyleCop>true</EnableStyleCop>

<!-- Do not include the generator as a lib dependency -->
<IncludeBuildOutput>false</IncludeBuildOutput>
Expand All @@ -29,6 +30,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" Version="3.11.0-beta1.24165.2" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
15 changes: 15 additions & 0 deletions src/Analyzers/AnalyzersCategories.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask.Analyzers;

/// <summary>
/// Provides a set of well-known categories that are used by the analyzers diagnostics.
/// </summary>
static class AnalyzersCategories
{
/// <summary>
/// The category for the orchestration related analyzers.
/// </summary>
public const string Orchestration = "Orchestration";
}
58 changes: 58 additions & 0 deletions src/Analyzers/KnownTypeSymbols.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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>
sealed class KnownTypeSymbols(Compilation compilation)
{
readonly Compilation compilation = compilation;

INamedTypeSymbol? functionOrchestrationAttribute;
INamedTypeSymbol? functionNameAttribute;
INamedTypeSymbol? taskOrchestratorInterface;
INamedTypeSymbol? taskOrchestratorBaseClass;
INamedTypeSymbol? durableTaskRegistry;

/// <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 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);

INamedTypeSymbol? GetOrResolveFullyQualifiedType(string fullyQualifiedName, ref INamedTypeSymbol? field)
{
if (field != null)
{
return field;
}

return field = this.compilation.GetTypeByMetadataName(fullyQualifiedName);
}
}
87 changes: 87 additions & 0 deletions src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Concurrent;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.DurableTask.Analyzers.Orchestration;

/// <summary>
/// Analyzer that reports a warning when a non-deterministic DateTime property is used in an orchestration method.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer
{
/// <summary>
/// Diagnostic ID supported for the analyzer.
/// </summary>
public const string DiagnosticId = "DURABLE0001";

static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.DateTimeOrchestrationAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.DateTimeOrchestrationAnalyzerMessageFormat), 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];

/// <inheritdoc/>
protected override void RegisterAdditionalCompilationStartAction(CompilationStartAnalysisContext context, OrchestrationAnalysisResult orchestrationAnalysisResult)
{
INamedTypeSymbol systemDateTimeSymbol = context.Compilation.GetSpecialType(SpecialType.System_DateTime);

// stores the symbols (such as methods) and the DateTime references used in them
ConcurrentBag<(ISymbol Symbol, IPropertyReferenceOperation Operation)> dateTimeUsage = [];

// search for usages of DateTime.Now, DateTime.UtcNow, DateTime.Today and store them
context.RegisterOperationAction(
ctx =>
{
ctx.CancellationToken.ThrowIfCancellationRequested();

var operation = (IPropertyReferenceOperation)ctx.Operation;
IPropertySymbol property = operation.Property;

if (!property.ContainingSymbol.Equals(systemDateTimeSymbol, SymbolEqualityComparer.Default))
{
return;
}

if (property.Name is nameof(DateTime.Now) or nameof(DateTime.UtcNow) or nameof(DateTime.Today))
{
ISymbol method = ctx.ContainingSymbol;
dateTimeUsage.Add((method, operation));
}
},
OperationKind.PropertyReference);

// compare whether the found DateTime usages occur in methods invoked by orchestrations
context.RegisterCompilationEndAction(ctx =>
{
foreach ((ISymbol symbol, IPropertyReferenceOperation operation) in dateTimeUsage)
{
if (symbol is IMethodSymbol method)
{
if (orchestrationAnalysisResult.OrchestrationsByMethod.TryGetValue(method, out ConcurrentBag<AnalyzedOrchestration> orchestrations))
{
string methodName = symbol.Name;
string dateTimePropertyName = operation.Property.ToString();
string orchestrationNames = string.Join(", ", orchestrations.Select(o => o.Name).OrderBy(n => n));

// e.g.: "The method 'Method1' uses 'System.Date.Now' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
ctx.ReportDiagnostic(Rule, operation, methodName, dateTimePropertyName, orchestrationNames);
}
}
}
});
}
}
Loading

0 comments on commit a275358

Please sign in to comment.