From 447e14d51abf1db2a905526efdb9cb433e450719 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:26:30 +0000 Subject: [PATCH 1/3] Initial plan From ee22dec0daa48167e108a2523f9d2f24904c6d18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:36:31 +0000 Subject: [PATCH 2/3] Add DateTimeOffset.Now and DateTimeOffset.UtcNow detection to analyzer Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../DateTimeOrchestrationAnalyzer.cs | 17 +++-- .../DateTimeOrchestrationFixer.cs | 20 ++++- .../DateTimeOrchestrationAnalyzerTests.cs | 76 +++++++++++++++++++ 3 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs index 7eefeb67..2d0364c7 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs @@ -10,7 +10,7 @@ namespace Microsoft.DurableTask.Analyzers.Orchestration; /// -/// 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. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer @@ -35,16 +35,18 @@ public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer SupportedDiagnostics => [Rule]; /// - /// Visitor that inspects the method body for DateTime properties. + /// Visitor that inspects the method body for DateTime and DateTimeOffset properties. /// public sealed class DateTimeOrchestrationVisitor : MethodProbeOrchestrationVisitor { INamedTypeSymbol systemDateTimeSymbol = null!; + INamedTypeSymbol? systemDateTimeOffsetSymbol; /// public override bool Initialize() { this.systemDateTimeSymbol = this.Compilation.GetSpecialType(SpecialType.System_DateTime); + this.systemDateTimeOffsetSymbol = this.Compilation.GetTypeByMetadataName("System.DateTimeOffset"); return true; } @@ -61,14 +63,19 @@ protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode meth { 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)) { - // 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)); } } diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs index 5b32857c..cf8ceb7a 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs @@ -26,7 +26,7 @@ public sealed class DateTimeOrchestrationFixer : OrchestrationContextFixer /// 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; @@ -35,12 +35,15 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC // Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx") string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name; + // Check if this is a DateTimeOffset expression + bool isDateTimeOffset = dateTimeExpression.Expression.ToString().Contains("DateTimeOffset"); bool isDateTimeToday = dateTimeExpression.Name.ToString() == "Today"; string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty; string 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 'context.CurrentUtcDateTime' instead of 'DateTimeOffset.Now'" string title = string.Format( CultureInfo.InvariantCulture, Resources.UseInsteadFixerTitle, @@ -50,15 +53,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 ReplaceDateTime(Document document, SyntaxNode oldRoot, MemberAccessExpressionSyntax incorrectDateTimeSyntax, string contextParameterName, bool isDateTimeToday) + static Task 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), @@ -73,6 +76,15 @@ static Task 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); diff --git a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs index 0f5fd842..4c5a1889 100644 --- a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs +++ b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs @@ -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 +{ + public override Task RunAsync(TaskOrchestrationContext context, string input) + { + return Task.FromResult({|#0:DateTimeOffset.Now|}); + } +} +"); + + string fix = Wrapper.WrapTaskOrchestrator(@" +public class MyOrchestrator : TaskOrchestrator +{ + public override Task 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); From 3df21491bb376dedab45c90aa737e8e88b981408 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:39:49 +0000 Subject: [PATCH 3/3] Address code review feedback - improve type detection and fix Today property handling Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../DateTimeOrchestrationAnalyzer.cs | 8 +++++- .../DateTimeOrchestrationFixer.cs | 25 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs index 2d0364c7..d6555481 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs @@ -72,7 +72,13 @@ protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode meth 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.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'" diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs index cf8ceb7a..fd233d1b 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs @@ -35,15 +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; - // Check if this is a DateTimeOffset expression - bool isDateTimeOffset = dateTimeExpression.Expression.ToString().Contains("DateTimeOffset"); + // 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 'context.CurrentUtcDateTime' instead of 'DateTimeOffset.Now'" + // e.g: "Use '(DateTimeOffset)context.CurrentUtcDateTime' instead of 'DateTimeOffset.Now'" string title = string.Format( CultureInfo.InvariantCulture, Resources.UseInsteadFixerTitle,