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

Non-deterministic System.DateTime usage Roslyn Analyzer #284

Merged
merged 21 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7ef6afd
datetime analyzer
allantargino Apr 5, 2024
b107350
adding xunit package
allantargino Apr 8, 2024
74b6b3d
renaming Analyzers.Test to Analyzers.Tests
allantargino Apr 8, 2024
abe7f76
supporting class based orchestrators
allantargino Apr 9, 2024
4c029fb
commenting implementation details
allantargino Apr 10, 2024
88ff4a0
flatten helpers folder structure
allantargino Apr 10, 2024
9365704
removing cache wrapper from KnownTypeSymbols
allantargino Apr 10, 2024
838eef4
moving strings to resources file
allantargino Apr 10, 2024
e80a6c8
reusing test case
allantargino Apr 10, 2024
a3d3799
fix namespaces + small rewrite for better readability
allantargino Apr 10, 2024
77a9be7
renaming roslyn method for better clarity
allantargino Apr 11, 2024
ddfa241
using explicit type + new convention
allantargino Apr 11, 2024
90e9def
guards in case RunAsync methods are not available in analyzer
allantargino Apr 11, 2024
4c97c96
Resource.Designer.cs created by code generation
allantargino Apr 11, 2024
da05012
enabling StyleCop analyzers and fixing style issues
allantargino Apr 12, 2024
f0120f3
updating stylecop analyzers versions
allantargino Apr 12, 2024
1dfb481
documenting members
allantargino Apr 12, 2024
47f0fb9
removing resx embedded resource from analyzers
allantargino Apr 12, 2024
d7274bd
Update src/Analyzers/Orchestration/OrchestrationAnalyzer.cs
allantargino Apr 17, 2024
6a2dd99
fixing comments grammar/additional comments
allantargino Apr 17, 2024
9175423
Merge branch 'datetime-analyzer' of github.com:allantargino/durableta…
allantargino Apr 17, 2024
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
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
allantargino marked this conversation as resolved.
Show resolved Hide resolved
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" />
bachuv marked this conversation as resolved.
Show resolved Hide resolved
</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);
allantargino marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets a DurableTaskRegistry type symbol.
/// </summary>
public INamedTypeSymbol? DurableTaskRegistry => this.GetOrResolveFullyQualifiedType("Microsoft.DurableTask.DurableTaskRegistry", ref this.durableTaskRegistry);
allantargino marked this conversation as resolved.
Show resolved Hide resolved

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
allantargino marked this conversation as resolved.
Show resolved Hide resolved
{
/// <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))
allantargino marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading