Skip to content

Commit 310c8e9

Browse files
authored
Add DateTimeOffset.Now and DateTimeOffset.UtcNow detection to Roslyn analyzer (#547)
1 parent c8705e8 commit 310c8e9

File tree

3 files changed

+128
-12
lines changed

3 files changed

+128
-12
lines changed

src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
namespace Microsoft.DurableTask.Analyzers.Orchestration;
1111

1212
/// <summary>
13-
/// Analyzer that reports a warning when a non-deterministic DateTime property is used in an orchestration method.
13+
/// Analyzer that reports a warning when a non-deterministic DateTime or DateTimeOffset property is used in an orchestration method.
1414
/// </summary>
1515
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1616
public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer<DateTimeOrchestrationVisitor>
@@ -35,16 +35,18 @@ public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer<DateTi
3535
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
3636

3737
/// <summary>
38-
/// Visitor that inspects the method body for DateTime properties.
38+
/// Visitor that inspects the method body for DateTime and DateTimeOffset properties.
3939
/// </summary>
4040
public sealed class DateTimeOrchestrationVisitor : MethodProbeOrchestrationVisitor
4141
{
4242
INamedTypeSymbol systemDateTimeSymbol = null!;
43+
INamedTypeSymbol? systemDateTimeOffsetSymbol;
4344

4445
/// <inheritdoc/>
4546
public override bool Initialize()
4647
{
4748
this.systemDateTimeSymbol = this.Compilation.GetSpecialType(SpecialType.System_DateTime);
49+
this.systemDateTimeOffsetSymbol = this.Compilation.GetTypeByMetadataName("System.DateTimeOffset");
4850
return true;
4951
}
5052

@@ -61,14 +63,25 @@ protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode meth
6163
{
6264
IPropertySymbol property = operation.Property;
6365

64-
if (!property.ContainingSymbol.Equals(this.systemDateTimeSymbol, SymbolEqualityComparer.Default))
66+
bool isDateTime = property.ContainingSymbol.Equals(this.systemDateTimeSymbol, SymbolEqualityComparer.Default);
67+
bool isDateTimeOffset = this.systemDateTimeOffsetSymbol is not null &&
68+
property.ContainingSymbol.Equals(this.systemDateTimeOffsetSymbol, SymbolEqualityComparer.Default);
69+
70+
if (!isDateTime && !isDateTimeOffset)
6571
{
66-
return;
72+
continue;
6773
}
6874

69-
if (property.Name is nameof(DateTime.Now) or nameof(DateTime.UtcNow) or nameof(DateTime.Today))
75+
// Check for non-deterministic properties
76+
// DateTime has: Now, UtcNow, Today
77+
// DateTimeOffset has: Now, UtcNow (but not Today)
78+
bool isNonDeterministic = property.Name is nameof(DateTime.Now) or nameof(DateTime.UtcNow) ||
79+
(isDateTime && property.Name == nameof(DateTime.Today));
80+
81+
if (isNonDeterministic)
7082
{
71-
// e.g.: "The method 'Method1' uses 'System.Date.Now' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
83+
// e.g.: "The method 'Method1' uses 'System.DateTime.Now' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
84+
// e.g.: "The method 'Method1' uses 'System.DateTimeOffset.Now' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'"
7285
reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation.Syntax, methodSymbol.Name, property.ToString(), orchestrationName));
7386
}
7487
}

src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public sealed class DateTimeOrchestrationFixer : OrchestrationContextFixer
2626
/// <inheritdoc/>
2727
protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext)
2828
{
29-
// Parses the syntax node to see if it is a member access expression (e.g. DateTime.Now)
29+
// Parses the syntax node to see if it is a member access expression (e.g. DateTime.Now or DateTimeOffset.Now)
3030
if (orchestrationContext.SyntaxNodeWithDiagnostic is not MemberAccessExpressionSyntax dateTimeExpression)
3131
{
3232
return;
@@ -35,12 +35,30 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC
3535
// Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx")
3636
string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name;
3737

38+
// Use semantic analysis to determine if this is a DateTimeOffset expression
39+
SemanticModel semanticModel = orchestrationContext.SemanticModel;
40+
ITypeSymbol? typeSymbol = semanticModel.GetTypeInfo(dateTimeExpression.Expression).Type;
41+
bool isDateTimeOffset = typeSymbol?.ToDisplayString() == "System.DateTimeOffset";
42+
3843
bool isDateTimeToday = dateTimeExpression.Name.ToString() == "Today";
39-
string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty;
40-
string recommendation = $"{contextParameterName}.CurrentUtcDateTime{dateTimeTodaySuffix}";
44+
45+
// Build the recommendation text
46+
string recommendation;
47+
if (isDateTimeOffset)
48+
{
49+
// For DateTimeOffset, we always just cast CurrentUtcDateTime
50+
recommendation = $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime";
51+
}
52+
else
53+
{
54+
// For DateTime, we may need to add .Date for Today
55+
string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty;
56+
recommendation = $"{contextParameterName}.CurrentUtcDateTime{dateTimeTodaySuffix}";
57+
}
4158

4259
// e.g: "Use 'context.CurrentUtcDateTime' instead of 'DateTime.Now'"
4360
// e.g: "Use 'context.CurrentUtcDateTime.Date' instead of 'DateTime.Today'"
61+
// e.g: "Use '(DateTimeOffset)context.CurrentUtcDateTime' instead of 'DateTimeOffset.Now'"
4462
string title = string.Format(
4563
CultureInfo.InvariantCulture,
4664
Resources.UseInsteadFixerTitle,
@@ -50,15 +68,15 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC
5068
context.RegisterCodeFix(
5169
CodeAction.Create(
5270
title: title,
53-
createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday),
71+
createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday, isDateTimeOffset),
5472
equivalenceKey: title), // This key is used to prevent duplicate code fixes.
5573
context.Diagnostics);
5674
}
5775

58-
static Task<Document> ReplaceDateTime(Document document, SyntaxNode oldRoot, MemberAccessExpressionSyntax incorrectDateTimeSyntax, string contextParameterName, bool isDateTimeToday)
76+
static Task<Document> ReplaceDateTime(Document document, SyntaxNode oldRoot, MemberAccessExpressionSyntax incorrectDateTimeSyntax, string contextParameterName, bool isDateTimeToday, bool isDateTimeOffset)
5977
{
6078
// Builds a 'context.CurrentUtcDateTime' syntax node
61-
MemberAccessExpressionSyntax correctDateTimeSyntax =
79+
ExpressionSyntax correctDateTimeSyntax =
6280
MemberAccessExpression(
6381
SyntaxKind.SimpleMemberAccessExpression,
6482
IdentifierName(contextParameterName),
@@ -73,6 +91,15 @@ static Task<Document> ReplaceDateTime(Document document, SyntaxNode oldRoot, Mem
7391
IdentifierName("Date"));
7492
}
7593

94+
// If the original expression was DateTimeOffset, we need to cast the DateTime to DateTimeOffset
95+
// This is done using a CastExpression: (DateTimeOffset)context.CurrentUtcDateTime
96+
if (isDateTimeOffset)
97+
{
98+
correctDateTimeSyntax = CastExpression(
99+
IdentifierName("DateTimeOffset"),
100+
correctDateTimeSyntax);
101+
}
102+
76103
// Replaces the old local declaration with the new local declaration.
77104
SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectDateTimeSyntax, correctDateTimeSyntax);
78105
Document newDocument = document.WithSyntaxRoot(newRoot);

test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,82 @@ await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix, test =>
377377
}
378378

379379

380+
[Theory]
381+
[InlineData("DateTimeOffset.Now")]
382+
[InlineData("DateTimeOffset.UtcNow")]
383+
public async Task DurableFunctionOrchestrationUsingDateTimeOffsetNonDeterministicPropertiesHasDiag(string expression)
384+
{
385+
string code = Wrapper.WrapDurableFunctionOrchestration($@"
386+
[Function(""Run"")]
387+
DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context)
388+
{{
389+
return {{|#0:{expression}|}};
390+
}}
391+
");
392+
393+
string fix = Wrapper.WrapDurableFunctionOrchestration($@"
394+
[Function(""Run"")]
395+
DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context)
396+
{{
397+
return (DateTimeOffset)context.CurrentUtcDateTime;
398+
}}
399+
");
400+
401+
DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", $"System.{expression}", "Run");
402+
403+
await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix);
404+
}
405+
406+
[Fact]
407+
public async Task TaskOrchestratorUsingDateTimeOffsetHasDiag()
408+
{
409+
string code = Wrapper.WrapTaskOrchestrator(@"
410+
public class MyOrchestrator : TaskOrchestrator<string, DateTimeOffset>
411+
{
412+
public override Task<DateTimeOffset> RunAsync(TaskOrchestrationContext context, string input)
413+
{
414+
return Task.FromResult({|#0:DateTimeOffset.Now|});
415+
}
416+
}
417+
");
418+
419+
string fix = Wrapper.WrapTaskOrchestrator(@"
420+
public class MyOrchestrator : TaskOrchestrator<string, DateTimeOffset>
421+
{
422+
public override Task<DateTimeOffset> RunAsync(TaskOrchestrationContext context, string input)
423+
{
424+
return Task.FromResult((DateTimeOffset)context.CurrentUtcDateTime);
425+
}
426+
}
427+
");
428+
429+
DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.DateTimeOffset.Now", "MyOrchestrator");
430+
431+
await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix);
432+
}
433+
434+
[Fact]
435+
public async Task FuncOrchestratorWithDateTimeOffsetHasDiag()
436+
{
437+
string code = Wrapper.WrapFuncOrchestrator(@"
438+
tasks.AddOrchestratorFunc(""HelloSequence"", context =>
439+
{
440+
return {|#0:DateTimeOffset.UtcNow|};
441+
});
442+
");
443+
444+
string fix = Wrapper.WrapFuncOrchestrator(@"
445+
tasks.AddOrchestratorFunc(""HelloSequence"", context =>
446+
{
447+
return (DateTimeOffset)context.CurrentUtcDateTime;
448+
});
449+
");
450+
451+
DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.DateTimeOffset.UtcNow", "HelloSequence");
452+
453+
await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix);
454+
}
455+
380456
static DiagnosticResult BuildDiagnostic()
381457
{
382458
return VerifyCS.Diagnostic(DateTimeOrchestrationAnalyzer.DiagnosticId);

0 commit comments

Comments
 (0)