Skip to content
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
25 changes: 19 additions & 6 deletions src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
namespace Microsoft.DurableTask.Analyzers.Orchestration;

/// <summary>
/// Analyzer that reports a warning when a non-deterministic DateTime property is used in an orchestration method.
/// Analyzer that reports a warning when a non-deterministic DateTime or DateTimeOffset property is used in an orchestration method.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]

Check warning on line 15 in src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

This compiler extension should not be implemented in an assembly containing a reference to Microsoft.CodeAnalysis.Workspaces. The Microsoft.CodeAnalysis.Workspaces assembly is not provided during command line compilation scenarios, so references to it could cause the compiler extension to behave unpredictably. (https://github.com/dotnet/roslyn-analyzers/blob/main/docs/rules/RS1038.md)
public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer<DateTimeOrchestrationVisitor>
{
/// <summary>
Expand All @@ -35,16 +35,18 @@
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];

/// <summary>
/// Visitor that inspects the method body for DateTime properties.
/// Visitor that inspects the method body for DateTime and DateTimeOffset properties.
/// </summary>
public sealed class DateTimeOrchestrationVisitor : MethodProbeOrchestrationVisitor
{
INamedTypeSymbol systemDateTimeSymbol = null!;
INamedTypeSymbol? systemDateTimeOffsetSymbol;

/// <inheritdoc/>
public override bool Initialize()
{
this.systemDateTimeSymbol = this.Compilation.GetSpecialType(SpecialType.System_DateTime);
this.systemDateTimeOffsetSymbol = this.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
return true;
}

Expand All @@ -61,14 +63,25 @@
{
IPropertySymbol property = operation.Property;

if (!property.ContainingSymbol.Equals(this.systemDateTimeSymbol, SymbolEqualityComparer.Default))
bool isDateTime = property.ContainingSymbol.Equals(this.systemDateTimeSymbol, SymbolEqualityComparer.Default);
bool isDateTimeOffset = this.systemDateTimeOffsetSymbol is not null &&
property.ContainingSymbol.Equals(this.systemDateTimeOffsetSymbol, SymbolEqualityComparer.Default);

if (!isDateTime && !isDateTimeOffset)
{
return;
continue;
}

if (property.Name is nameof(DateTime.Now) or nameof(DateTime.UtcNow) or nameof(DateTime.Today))
// Check for non-deterministic properties
// DateTime has: Now, UtcNow, Today
// DateTimeOffset has: Now, UtcNow (but not Today)
bool isNonDeterministic = property.Name is nameof(DateTime.Now) or nameof(DateTime.UtcNow) ||
(isDateTime && property.Name == nameof(DateTime.Today));

if (isNonDeterministic)
{
// e.g.: "The method 'Method1' uses 'System.Date.Now' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
// e.g.: "The method 'Method1' uses 'System.DateTime.Now' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
// e.g.: "The method 'Method1' uses 'System.DateTimeOffset.Now' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation.Syntax, methodSymbol.Name, property.ToString(), orchestrationName));
}
}
Expand Down
39 changes: 33 additions & 6 deletions src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public sealed class DateTimeOrchestrationFixer : OrchestrationContextFixer
/// <inheritdoc/>
protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext)
{
// Parses the syntax node to see if it is a member access expression (e.g. DateTime.Now)
// Parses the syntax node to see if it is a member access expression (e.g. DateTime.Now or DateTimeOffset.Now)
if (orchestrationContext.SyntaxNodeWithDiagnostic is not MemberAccessExpressionSyntax dateTimeExpression)
{
return;
Expand All @@ -35,12 +35,30 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC
// Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx")
string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name;

// Use semantic analysis to determine if this is a DateTimeOffset expression
SemanticModel semanticModel = orchestrationContext.SemanticModel;
ITypeSymbol? typeSymbol = semanticModel.GetTypeInfo(dateTimeExpression.Expression).Type;
bool isDateTimeOffset = typeSymbol?.ToDisplayString() == "System.DateTimeOffset";

bool isDateTimeToday = dateTimeExpression.Name.ToString() == "Today";
string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty;
string recommendation = $"{contextParameterName}.CurrentUtcDateTime{dateTimeTodaySuffix}";

// Build the recommendation text
string recommendation;
if (isDateTimeOffset)
{
// For DateTimeOffset, we always just cast CurrentUtcDateTime
recommendation = $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime";
}
else
{
// For DateTime, we may need to add .Date for Today
string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty;
recommendation = $"{contextParameterName}.CurrentUtcDateTime{dateTimeTodaySuffix}";
}

// e.g: "Use 'context.CurrentUtcDateTime' instead of 'DateTime.Now'"
// e.g: "Use 'context.CurrentUtcDateTime.Date' instead of 'DateTime.Today'"
// e.g: "Use '(DateTimeOffset)context.CurrentUtcDateTime' instead of 'DateTimeOffset.Now'"
string title = string.Format(
CultureInfo.InvariantCulture,
Resources.UseInsteadFixerTitle,
Expand All @@ -50,15 +68,15 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday),
createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday, isDateTimeOffset),
equivalenceKey: title), // This key is used to prevent duplicate code fixes.
context.Diagnostics);
}

static Task<Document> ReplaceDateTime(Document document, SyntaxNode oldRoot, MemberAccessExpressionSyntax incorrectDateTimeSyntax, string contextParameterName, bool isDateTimeToday)
static Task<Document> ReplaceDateTime(Document document, SyntaxNode oldRoot, MemberAccessExpressionSyntax incorrectDateTimeSyntax, string contextParameterName, bool isDateTimeToday, bool isDateTimeOffset)
{
// Builds a 'context.CurrentUtcDateTime' syntax node
MemberAccessExpressionSyntax correctDateTimeSyntax =
ExpressionSyntax correctDateTimeSyntax =
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(contextParameterName),
Expand All @@ -73,6 +91,15 @@ static Task<Document> ReplaceDateTime(Document document, SyntaxNode oldRoot, Mem
IdentifierName("Date"));
}

// If the original expression was DateTimeOffset, we need to cast the DateTime to DateTimeOffset
// This is done using a CastExpression: (DateTimeOffset)context.CurrentUtcDateTime
if (isDateTimeOffset)
{
correctDateTimeSyntax = CastExpression(
IdentifierName("DateTimeOffset"),
correctDateTimeSyntax);
}

// Replaces the old local declaration with the new local declaration.
SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectDateTimeSyntax, correctDateTimeSyntax);
Document newDocument = document.WithSyntaxRoot(newRoot);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,82 @@ await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix, test =>
}


[Theory]
[InlineData("DateTimeOffset.Now")]
[InlineData("DateTimeOffset.UtcNow")]
public async Task DurableFunctionOrchestrationUsingDateTimeOffsetNonDeterministicPropertiesHasDiag(string expression)
{
string code = Wrapper.WrapDurableFunctionOrchestration($@"
[Function(""Run"")]
DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context)
{{
return {{|#0:{expression}|}};
}}
");

string fix = Wrapper.WrapDurableFunctionOrchestration($@"
[Function(""Run"")]
DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context)
{{
return (DateTimeOffset)context.CurrentUtcDateTime;
}}
");

DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", $"System.{expression}", "Run");

await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix);
}

[Fact]
public async Task TaskOrchestratorUsingDateTimeOffsetHasDiag()
{
string code = Wrapper.WrapTaskOrchestrator(@"
public class MyOrchestrator : TaskOrchestrator<string, DateTimeOffset>
{
public override Task<DateTimeOffset> RunAsync(TaskOrchestrationContext context, string input)
{
return Task.FromResult({|#0:DateTimeOffset.Now|});
}
}
");

string fix = Wrapper.WrapTaskOrchestrator(@"
public class MyOrchestrator : TaskOrchestrator<string, DateTimeOffset>
{
public override Task<DateTimeOffset> RunAsync(TaskOrchestrationContext context, string input)
{
return Task.FromResult((DateTimeOffset)context.CurrentUtcDateTime);
}
}
");

DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.DateTimeOffset.Now", "MyOrchestrator");

await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix);
}

[Fact]
public async Task FuncOrchestratorWithDateTimeOffsetHasDiag()
{
string code = Wrapper.WrapFuncOrchestrator(@"
tasks.AddOrchestratorFunc(""HelloSequence"", context =>
{
return {|#0:DateTimeOffset.UtcNow|};
});
");

string fix = Wrapper.WrapFuncOrchestrator(@"
tasks.AddOrchestratorFunc(""HelloSequence"", context =>
{
return (DateTimeOffset)context.CurrentUtcDateTime;
});
");

DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.DateTimeOffset.UtcNow", "HelloSequence");

await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix);
}

static DiagnosticResult BuildDiagnostic()
{
return VerifyCS.Diagnostic(DateTimeOrchestrationAnalyzer.DiagnosticId);
Expand Down
Loading