From afba9a9441cbc6d1c6e26f317de7488067d045bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:19:49 +0000 Subject: [PATCH 1/8] Initial plan From 70b84c3fd9556b6452decc9ca2f663a90da3c8bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:23:37 +0000 Subject: [PATCH 2/8] Update global.json to use available .NET SDK version Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 332b6c45..4b560e68 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.101", + "version": "10.0.100", "rollForward": "latestFeature" }, "msbuild-sdks": { From 0fc65758a624cb4fd34418b1b16377ce3375bbd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:34:01 +0000 Subject: [PATCH 3/8] Add TimeProvider support to DateTimeOrchestrationAnalyzer Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- src/Analyzers/KnownTypeSymbols.Net.cs | 6 + .../DateTimeOrchestrationAnalyzer.cs | 31 +++- .../DateTimeOrchestrationFixer.cs | 155 +++++++++++++----- .../DateTimeOrchestrationAnalyzerTests.cs | 124 ++++++++++++++ test/Analyzers.Tests/Verifiers/References.cs | 2 +- 5 files changed, 279 insertions(+), 39 deletions(-) diff --git a/src/Analyzers/KnownTypeSymbols.Net.cs b/src/Analyzers/KnownTypeSymbols.Net.cs index 9803273a..f39ce260 100644 --- a/src/Analyzers/KnownTypeSymbols.Net.cs +++ b/src/Analyzers/KnownTypeSymbols.Net.cs @@ -23,6 +23,7 @@ public sealed partial class KnownTypeSymbols INamedTypeSymbol? cancellationToken; INamedTypeSymbol? environment; INamedTypeSymbol? httpClient; + INamedTypeSymbol? timeProvider; /// /// Gets a Guid type symbol. @@ -75,4 +76,9 @@ public sealed partial class KnownTypeSymbols /// Gets an HttpClient type symbol. /// public INamedTypeSymbol? HttpClient => this.GetOrResolveFullyQualifiedType(typeof(HttpClient).FullName, ref this.httpClient); + + /// + /// Gets a TimeProvider type symbol. + /// + public INamedTypeSymbol? TimeProvider => this.GetOrResolveFullyQualifiedType("System.TimeProvider", ref this.timeProvider); } diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs index d6555481..55571730 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 or DateTimeOffset property is used in an orchestration method. +/// Analyzer that reports a warning when a non-deterministic DateTime, DateTimeOffset, or TimeProvider method is used in an orchestration method. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer @@ -41,12 +41,14 @@ public sealed class DateTimeOrchestrationVisitor : MethodProbeOrchestrationVisit { INamedTypeSymbol systemDateTimeSymbol = null!; INamedTypeSymbol? systemDateTimeOffsetSymbol; + INamedTypeSymbol? systemTimeProviderSymbol; /// public override bool Initialize() { this.systemDateTimeSymbol = this.Compilation.GetSpecialType(SpecialType.System_DateTime); this.systemDateTimeOffsetSymbol = this.Compilation.GetTypeByMetadataName("System.DateTimeOffset"); + this.systemTimeProviderSymbol = this.Compilation.GetTypeByMetadataName("System.TimeProvider"); return true; } @@ -85,6 +87,33 @@ protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode meth reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation.Syntax, methodSymbol.Name, property.ToString(), orchestrationName)); } } + + // Check for TimeProvider method invocations + if (this.systemTimeProviderSymbol is not null) + { + foreach (IInvocationOperation operation in methodOperation.Descendants().OfType()) + { + IMethodSymbol invokedMethod = operation.TargetMethod; + + // Check if the method is called on TimeProvider type + bool isTimeProvider = invokedMethod.ContainingType.Equals(this.systemTimeProviderSymbol, SymbolEqualityComparer.Default); + + if (!isTimeProvider) + { + continue; + } + + // Check for non-deterministic TimeProvider methods: GetUtcNow, GetLocalNow, GetTimestamp + bool isNonDeterministicMethod = invokedMethod.Name is "GetUtcNow" or "GetLocalNow" or "GetTimestamp"; + + if (isNonDeterministicMethod) + { + // e.g.: "The method 'Method1' uses 'System.TimeProvider.GetUtcNow()' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'" + string timeProviderMethodName = $"{invokedMethod.ContainingType}.{invokedMethod.Name}()"; + reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation.Syntax, methodSymbol.Name, timeProviderMethodName, orchestrationName)); + } + } + } } } } diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs index fd233d1b..6ea59d5e 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs @@ -26,51 +26,87 @@ 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 or DateTimeOffset.Now) - if (orchestrationContext.SyntaxNodeWithDiagnostic is not MemberAccessExpressionSyntax dateTimeExpression) - { - return; - } - // 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"; - // Build the recommendation text - string recommendation; - if (isDateTimeOffset) + // Handle DateTime/DateTimeOffset property access (e.g. DateTime.Now or DateTimeOffset.Now) + if (orchestrationContext.SyntaxNodeWithDiagnostic is MemberAccessExpressionSyntax dateTimeExpression) { - // For DateTimeOffset, we always just cast CurrentUtcDateTime - recommendation = $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime"; + // Use semantic analysis to determine if this is a DateTimeOffset expression + ITypeSymbol? typeSymbol = semanticModel.GetTypeInfo(dateTimeExpression.Expression).Type; + bool isDateTimeOffset = typeSymbol?.ToDisplayString() == "System.DateTimeOffset"; + + bool isDateTimeToday = dateTimeExpression.Name.ToString() == "Today"; + + // 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, + recommendation, + dateTimeExpression.ToString()); + + context.RegisterCodeFix( + CodeAction.Create( + title: title, + createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday, isDateTimeOffset), + equivalenceKey: title), // This key is used to prevent duplicate code fixes. + context.Diagnostics); } - else + + // Handle TimeProvider method invocations (e.g. TimeProvider.System.GetUtcNow()) + else if (orchestrationContext.SyntaxNodeWithDiagnostic is InvocationExpressionSyntax timeProviderInvocation) { - // For DateTime, we may need to add .Date for Today - string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty; - recommendation = $"{contextParameterName}.CurrentUtcDateTime{dateTimeTodaySuffix}"; - } + // Determine the method being called + if (semanticModel.GetSymbolInfo(timeProviderInvocation).Symbol is IMethodSymbol methodSymbol) + { + string methodName = methodSymbol.Name; + + // Check if the method returns DateTimeOffset + bool returnsDateTimeOffset = methodSymbol.ReturnType.ToDisplayString() == "System.DateTimeOffset"; - // 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, - recommendation, - dateTimeExpression.ToString()); - - context.RegisterCodeFix( - CodeAction.Create( - title: title, - createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday, isDateTimeOffset), - equivalenceKey: title), // This key is used to prevent duplicate code fixes. - context.Diagnostics); + // Build the recommendation based on the method name + string recommendation = methodName switch + { + "GetUtcNow" when returnsDateTimeOffset => $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime", + "GetUtcNow" => $"{contextParameterName}.CurrentUtcDateTime", + "GetLocalNow" when returnsDateTimeOffset => $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime.ToLocalTime()", + "GetLocalNow" => $"{contextParameterName}.CurrentUtcDateTime.ToLocalTime()", + "GetTimestamp" => $"{contextParameterName}.CurrentUtcDateTime.Ticks", + _ => $"{contextParameterName}.CurrentUtcDateTime", + }; + + // e.g: "Use 'context.CurrentUtcDateTime' instead of 'TimeProvider.System.GetUtcNow()'" + string title = string.Format( + CultureInfo.InvariantCulture, + Resources.UseInsteadFixerTitle, + recommendation, + timeProviderInvocation.ToString()); + + context.RegisterCodeFix( + CodeAction.Create( + title: title, + createChangedDocument: c => ReplaceTimeProvider(context.Document, orchestrationContext.Root, timeProviderInvocation, contextParameterName, methodName, returnsDateTimeOffset), + equivalenceKey: title), + context.Diagnostics); + } + } } static Task ReplaceDateTime(Document document, SyntaxNode oldRoot, MemberAccessExpressionSyntax incorrectDateTimeSyntax, string contextParameterName, bool isDateTimeToday, bool isDateTimeOffset) @@ -106,4 +142,49 @@ static Task ReplaceDateTime(Document document, SyntaxNode oldRoot, Mem return Task.FromResult(newDocument); } + + static Task ReplaceTimeProvider(Document document, SyntaxNode oldRoot, InvocationExpressionSyntax incorrectTimeProviderSyntax, string contextParameterName, string methodName, bool returnsDateTimeOffset) + { + // Build the correct expression based on the method name + ExpressionSyntax correctExpression = methodName switch + { + "GetUtcNow" => MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")), + "GetLocalNow" => InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")), + IdentifierName("ToLocalTime"))), + "GetTimestamp" => MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")), + IdentifierName("Ticks")), + _ => MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")), + }; + + // If the method returns DateTimeOffset, we need to cast the DateTime to DateTimeOffset + if (returnsDateTimeOffset) + { + correctExpression = CastExpression( + IdentifierName("DateTimeOffset"), + correctExpression); + } + + // Replaces the old invocation with the new expression + SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectTimeProviderSyntax, correctExpression); + Document newDocument = document.WithSyntaxRoot(newRoot); + + return Task.FromResult(newDocument); + } } diff --git a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs index 4c5a1889..6989b86a 100644 --- a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs +++ b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs @@ -453,6 +453,130 @@ public async Task FuncOrchestratorWithDateTimeOffsetHasDiag() await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } + [Theory] + [InlineData("TimeProvider.System.GetUtcNow()")] + [InlineData("TimeProvider.System.GetLocalNow()")] + public async Task DurableFunctionOrchestrationUsingTimeProviderNonDeterministicMethodsHasDiag(string expression) + { + string code = Wrapper.WrapDurableFunctionOrchestration($@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return {{|#0:{expression}|}}; +}} +"); + + string expectedReplacement = expression.Contains("GetLocalNow") + ? "(DateTimeOffset)context.CurrentUtcDateTime.ToLocalTime()" + : "(DateTimeOffset)context.CurrentUtcDateTime"; + + string fix = Wrapper.WrapDurableFunctionOrchestration($@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return {expectedReplacement}; +}} +"); + + // The analyzer reports the method name as "System.TimeProvider.GetUtcNow()" or "System.TimeProvider.GetLocalNow()" + string methodName = expression.Contains("GetLocalNow") ? "System.TimeProvider.GetLocalNow()" : "System.TimeProvider.GetUtcNow()"; + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", methodName, "Run"); + + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task DurableFunctionOrchestrationUsingTimeProviderGetTimestampHasDiag() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +long Run([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return {|#0:TimeProvider.System.GetTimestamp()|}; +} +"); + + string fix = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +long Run([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return context.CurrentUtcDateTime.Ticks; +} +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", "System.TimeProvider.GetTimestamp()", "Run"); + + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task TaskOrchestratorUsingTimeProviderHasDiag() + { + string code = Wrapper.WrapTaskOrchestrator(@" +public class MyOrchestrator : TaskOrchestrator +{ + public override Task RunAsync(TaskOrchestrationContext context, string input) + { + return Task.FromResult({|#0:TimeProvider.System.GetUtcNow()|}); + } +} +"); + + 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.TimeProvider.GetUtcNow()", "MyOrchestrator"); + + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task FuncOrchestratorWithTimeProviderHasDiag() + { + string code = Wrapper.WrapFuncOrchestrator(@" +tasks.AddOrchestratorFunc(""HelloSequence"", context => +{ + return {|#0:TimeProvider.System.GetUtcNow()|}; +}); +"); + + string fix = Wrapper.WrapFuncOrchestrator(@" +tasks.AddOrchestratorFunc(""HelloSequence"", context => +{ + return (DateTimeOffset)context.CurrentUtcDateTime; +}); +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.TimeProvider.GetUtcNow()", "HelloSequence"); + + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task DurableFunctionOrchestrationInvokingMethodWithTimeProviderHasDiag() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return GetTime(); +} + +DateTimeOffset GetTime() => {|#0:TimeProvider.System.GetUtcNow()|}; +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("GetTime", "System.TimeProvider.GetUtcNow()", "Run"); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + static DiagnosticResult BuildDiagnostic() { return VerifyCS.Diagnostic(DateTimeOrchestrationAnalyzer.DiagnosticId); diff --git a/test/Analyzers.Tests/Verifiers/References.cs b/test/Analyzers.Tests/Verifiers/References.cs index 1df31e76..b6b33d93 100644 --- a/test/Analyzers.Tests/Verifiers/References.cs +++ b/test/Analyzers.Tests/Verifiers/References.cs @@ -11,7 +11,7 @@ public static class References public static ReferenceAssemblies CommonAssemblies => durableAssemblyReferences.Value; - static ReferenceAssemblies BuildReferenceAssemblies() => ReferenceAssemblies.Net.Net60.AddPackages([ + static ReferenceAssemblies BuildReferenceAssemblies() => ReferenceAssemblies.Net.Net80.AddPackages([ new PackageIdentity("Azure.Storage.Blobs", "12.17.0"), new PackageIdentity("Azure.Storage.Queues", "12.17.0"), new PackageIdentity("Azure.Data.Tables", "12.8.3"), From 32c472b8ec7fb9b0d712447ff92da2081d9f807f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Dec 2025 00:39:09 +0000 Subject: [PATCH 4/8] Update comment to include TimeProvider in visitor description Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs index 55571730..94b30e90 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs @@ -35,7 +35,7 @@ public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer SupportedDiagnostics => [Rule]; /// - /// Visitor that inspects the method body for DateTime and DateTimeOffset properties. + /// Visitor that inspects the method body for DateTime and DateTimeOffset properties, and TimeProvider method invocations. /// public sealed class DateTimeOrchestrationVisitor : MethodProbeOrchestrationVisitor { From 340e16366c9705e82e8a286c543acc473e2e33b6 Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 30 Dec 2025 17:39:26 -0800 Subject: [PATCH 5/8] Update global.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 4b560e68..332b6c45 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.100", + "version": "10.0.101", "rollForward": "latestFeature" }, "msbuild-sdks": { From 1ceb9129259b11d51a05ee342701c96a6b14fb06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 01:55:28 +0000 Subject: [PATCH 6/8] Address review comments: fix XML docs, remove redundant ToString(), combine if statements, restore test files Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- src/Analyzers/KnownTypeSymbols.Net.cs | 8 +- .../DateTimeOrchestrationFixer.cs | 65 +++++----- .../DateTimeOrchestrationAnalyzerTests.cs | 112 ++++++++++++++---- .../CSharpCodeFixVerifier.Durable.cs | 52 +++++++- test/Analyzers.Tests/Verifiers/References.cs | 17 +++ 5 files changed, 193 insertions(+), 61 deletions(-) diff --git a/src/Analyzers/KnownTypeSymbols.Net.cs b/src/Analyzers/KnownTypeSymbols.Net.cs index b063dffb..8e327319 100644 --- a/src/Analyzers/KnownTypeSymbols.Net.cs +++ b/src/Analyzers/KnownTypeSymbols.Net.cs @@ -79,10 +79,12 @@ public sealed partial class KnownTypeSymbols public INamedTypeSymbol? HttpClient => this.GetOrResolveFullyQualifiedType(typeof(HttpClient).FullName, ref this.httpClient); /// - /// Gets a TimeProvider type symbol. - /// - public INamedTypeSymbol? TimeProvider => this.GetOrResolveFullyQualifiedType("System.TimeProvider", ref this.timeProvider); /// Gets an ILogger type symbol. /// public INamedTypeSymbol? ILogger => this.GetOrResolveFullyQualifiedType("Microsoft.Extensions.Logging.ILogger", ref this.iLogger); + + /// + /// Gets a TimeProvider type symbol. + /// + public INamedTypeSymbol? TimeProvider => this.GetOrResolveFullyQualifiedType("System.TimeProvider", ref this.timeProvider); } diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs index 6ea59d5e..a7b3402f 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs @@ -60,7 +60,7 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC CultureInfo.InvariantCulture, Resources.UseInsteadFixerTitle, recommendation, - dateTimeExpression.ToString()); + dateTimeExpression); context.RegisterCodeFix( CodeAction.Create( @@ -71,41 +71,38 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC } // Handle TimeProvider method invocations (e.g. TimeProvider.System.GetUtcNow()) - else if (orchestrationContext.SyntaxNodeWithDiagnostic is InvocationExpressionSyntax timeProviderInvocation) + else if (orchestrationContext.SyntaxNodeWithDiagnostic is InvocationExpressionSyntax timeProviderInvocation && + semanticModel.GetSymbolInfo(timeProviderInvocation).Symbol is IMethodSymbol methodSymbol) { - // Determine the method being called - if (semanticModel.GetSymbolInfo(timeProviderInvocation).Symbol is IMethodSymbol methodSymbol) + string methodName = methodSymbol.Name; + + // Check if the method returns DateTimeOffset + bool returnsDateTimeOffset = methodSymbol.ReturnType.ToDisplayString() == "System.DateTimeOffset"; + + // Build the recommendation based on the method name + string recommendation = methodName switch { - string methodName = methodSymbol.Name; - - // Check if the method returns DateTimeOffset - bool returnsDateTimeOffset = methodSymbol.ReturnType.ToDisplayString() == "System.DateTimeOffset"; - - // Build the recommendation based on the method name - string recommendation = methodName switch - { - "GetUtcNow" when returnsDateTimeOffset => $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime", - "GetUtcNow" => $"{contextParameterName}.CurrentUtcDateTime", - "GetLocalNow" when returnsDateTimeOffset => $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime.ToLocalTime()", - "GetLocalNow" => $"{contextParameterName}.CurrentUtcDateTime.ToLocalTime()", - "GetTimestamp" => $"{contextParameterName}.CurrentUtcDateTime.Ticks", - _ => $"{contextParameterName}.CurrentUtcDateTime", - }; - - // e.g: "Use 'context.CurrentUtcDateTime' instead of 'TimeProvider.System.GetUtcNow()'" - string title = string.Format( - CultureInfo.InvariantCulture, - Resources.UseInsteadFixerTitle, - recommendation, - timeProviderInvocation.ToString()); - - context.RegisterCodeFix( - CodeAction.Create( - title: title, - createChangedDocument: c => ReplaceTimeProvider(context.Document, orchestrationContext.Root, timeProviderInvocation, contextParameterName, methodName, returnsDateTimeOffset), - equivalenceKey: title), - context.Diagnostics); - } + "GetUtcNow" when returnsDateTimeOffset => $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime", + "GetUtcNow" => $"{contextParameterName}.CurrentUtcDateTime", + "GetLocalNow" when returnsDateTimeOffset => $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime.ToLocalTime()", + "GetLocalNow" => $"{contextParameterName}.CurrentUtcDateTime.ToLocalTime()", + "GetTimestamp" => $"{contextParameterName}.CurrentUtcDateTime.Ticks", + _ => $"{contextParameterName}.CurrentUtcDateTime", + }; + + // e.g: "Use 'context.CurrentUtcDateTime' instead of 'TimeProvider.System.GetUtcNow()'" + string title = string.Format( + CultureInfo.InvariantCulture, + Resources.UseInsteadFixerTitle, + recommendation, + timeProviderInvocation); + + context.RegisterCodeFix( + CodeAction.Create( + title: title, + createChangedDocument: c => ReplaceTimeProvider(context.Document, orchestrationContext.Root, timeProviderInvocation, contextParameterName, methodName, returnsDateTimeOffset), + equivalenceKey: title), + context.Diagnostics); } } diff --git a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs index 4404dff8..e42fa379 100644 --- a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs +++ b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs @@ -453,56 +453,128 @@ public async Task FuncOrchestratorWithDateTimeOffsetHasDiag() await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } + [Theory] + [InlineData("TimeProvider.System.GetUtcNow()")] + [InlineData("TimeProvider.System.GetLocalNow()")] + public async Task DurableFunctionOrchestrationUsingTimeProviderNonDeterministicMethodsHasDiag(string expression) + { + string code = Wrapper.WrapDurableFunctionOrchestration($@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return {{|#0:{expression}|}}; +}} +"); + + string expectedReplacement = expression.Contains("GetLocalNow") + ? "(DateTimeOffset)context.CurrentUtcDateTime.ToLocalTime()" + : "(DateTimeOffset)context.CurrentUtcDateTime"; + + string fix = Wrapper.WrapDurableFunctionOrchestration($@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return {expectedReplacement}; +}} +"); + + // The analyzer reports the method name as "System.TimeProvider.GetUtcNow()" or "System.TimeProvider.GetLocalNow()" + string methodName = expression.Contains("GetLocalNow") ? "System.TimeProvider.GetLocalNow()" : "System.TimeProvider.GetUtcNow()"; + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", methodName, "Run"); + + await VerifyCS.VerifyNet80CodeFixAsync(code, expected, fix); + } + [Fact] - public async Task TaskOrchestratorSdkOnlyHasDiag() + public async Task DurableFunctionOrchestrationUsingTimeProviderGetTimestampHasDiag() { - // Tests that the analyzer works with SDK-only references (without Azure Functions assemblies) - string code = Wrapper.WrapTaskOrchestratorSdkOnly(@" -public class MyOrchestrator : TaskOrchestrator + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +long Run([OrchestrationTrigger] TaskOrchestrationContext context) { - public override Task RunAsync(TaskOrchestrationContext context, string input) + return {|#0:TimeProvider.System.GetTimestamp()|}; +} +"); + + string fix = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +long Run([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return context.CurrentUtcDateTime.Ticks; +} +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", "System.TimeProvider.GetTimestamp()", "Run"); + + await VerifyCS.VerifyNet80CodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task TaskOrchestratorUsingTimeProviderHasDiag() { - return Task.FromResult({|#0:DateTime.Now|}); + string code = Wrapper.WrapTaskOrchestrator(@" +public class MyOrchestrator : TaskOrchestrator +{ + public override Task RunAsync(TaskOrchestrationContext context, string input) + { + return Task.FromResult({|#0:TimeProvider.System.GetUtcNow()|}); } } "); - string fix = Wrapper.WrapTaskOrchestratorSdkOnly(@" -public class MyOrchestrator : TaskOrchestrator + string fix = Wrapper.WrapTaskOrchestrator(@" +public class MyOrchestrator : TaskOrchestrator { - public override Task RunAsync(TaskOrchestrationContext context, string input) + public override Task RunAsync(TaskOrchestrationContext context, string input) { - return Task.FromResult(context.CurrentUtcDateTime); + return Task.FromResult((DateTimeOffset)context.CurrentUtcDateTime); } } "); - DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.DateTime.Now", "MyOrchestrator"); + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.TimeProvider.GetUtcNow()", "MyOrchestrator"); - await VerifyCS.VerifySdkOnlyCodeFixAsync(code, expected, fix); + await VerifyCS.VerifyNet80CodeFixAsync(code, expected, fix); } [Fact] - public async Task FuncOrchestratorSdkOnlyWithLambdaHasDiag() + public async Task FuncOrchestratorWithTimeProviderHasDiag() { - // Tests that the analyzer works with SDK-only references (without Azure Functions assemblies) - string code = Wrapper.WrapFuncOrchestratorSdkOnly(@" + string code = Wrapper.WrapFuncOrchestrator(@" tasks.AddOrchestratorFunc(""HelloSequence"", context => { - return {|#0:DateTime.Now|}; + return {|#0:TimeProvider.System.GetUtcNow()|}; }); "); - string fix = Wrapper.WrapFuncOrchestratorSdkOnly(@" + string fix = Wrapper.WrapFuncOrchestrator(@" tasks.AddOrchestratorFunc(""HelloSequence"", context => { - return context.CurrentUtcDateTime; + return (DateTimeOffset)context.CurrentUtcDateTime; }); "); - DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.DateTime.Now", "HelloSequence"); + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.TimeProvider.GetUtcNow()", "HelloSequence"); + + await VerifyCS.VerifyNet80CodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task DurableFunctionOrchestrationInvokingMethodWithTimeProviderHasDiag() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return GetTime(); +} + +DateTimeOffset GetTime() => {|#0:TimeProvider.System.GetUtcNow()|}; +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("GetTime", "System.TimeProvider.GetUtcNow()", "Run"); - await VerifyCS.VerifySdkOnlyCodeFixAsync(code, expected, fix); + await VerifyCS.VerifyNet80AnalyzerAsync(code, expected); } static DiagnosticResult BuildDiagnostic() diff --git a/test/Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.Durable.cs b/test/Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.Durable.cs index b69fae93..552f7a13 100644 --- a/test/Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.Durable.cs +++ b/test/Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.Durable.cs @@ -1,7 +1,4 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; @@ -89,6 +86,53 @@ public static async Task VerifySdkOnlyCodeFixAsync( References.SdkOnlyAssemblies, configureTest); } + /// + /// Runs analyzer test with .NET 8.0 references for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static Task VerifyNet80AnalyzerAsync(string source, params DiagnosticResult[] expected) + { + return VerifyNet80AnalyzerAsync(source, null, expected); + } + + /// + /// Runs analyzer test with .NET 8.0 references for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static async Task VerifyNet80AnalyzerAsync( + string source, Action? configureTest = null, params DiagnosticResult[] expected) + { + await RunAsync(expected, new Test() + { + TestCode = source, + }, References.Net80Assemblies, configureTest); + } + + /// + /// Runs code fix test with .NET 8.0 references for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static Task VerifyNet80CodeFixAsync( + string source, DiagnosticResult expected, string fixedSource, Action? configureTest = null) + { + return VerifyNet80CodeFixAsync(source, [expected], fixedSource, configureTest); + } + + /// + /// Runs code fix test with .NET 8.0 references for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static async Task VerifyNet80CodeFixAsync( + string source, DiagnosticResult[] expected, string fixedSource, Action? configureTest = null) + { + await RunAsync(expected, new Test() + { + TestCode = source, + FixedCode = fixedSource, + }, + References.Net80Assemblies, configureTest); + } + static async Task RunAsync(DiagnosticResult[] expected, Test test, ReferenceAssemblies referenceAssemblies, Action? configureTest = null) { test.ReferenceAssemblies = referenceAssemblies; diff --git a/test/Analyzers.Tests/Verifiers/References.cs b/test/Analyzers.Tests/Verifiers/References.cs index 59de0dda..701a04cf 100644 --- a/test/Analyzers.Tests/Verifiers/References.cs +++ b/test/Analyzers.Tests/Verifiers/References.cs @@ -9,6 +9,7 @@ public static class References { static readonly Lazy durableAssemblyReferences = new(() => BuildReferenceAssemblies()); static readonly Lazy durableSdkOnlyReferences = new(() => BuildSdkOnlyReferenceAssemblies()); + static readonly Lazy durableNet80References = new(() => BuildNet80ReferenceAssemblies()); public static ReferenceAssemblies CommonAssemblies => durableAssemblyReferences.Value; @@ -18,6 +19,12 @@ public static class References /// public static ReferenceAssemblies SdkOnlyAssemblies => durableSdkOnlyReferences.Value; + /// + /// Gets assembly references targeting .NET 8.0 for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static ReferenceAssemblies Net80Assemblies => durableNet80References.Value; + static ReferenceAssemblies BuildReferenceAssemblies() => ReferenceAssemblies.Net.Net60.AddPackages([ new PackageIdentity("Azure.Storage.Blobs", "12.17.0"), new PackageIdentity("Azure.Storage.Queues", "12.17.0"), @@ -37,4 +44,14 @@ static ReferenceAssemblies BuildSdkOnlyReferenceAssemblies() => ReferenceAssembl new PackageIdentity("Microsoft.DurableTask.Abstractions", "1.3.0"), new PackageIdentity("Microsoft.DurableTask.Worker", "1.3.0"), ]); + + static ReferenceAssemblies BuildNet80ReferenceAssemblies() => ReferenceAssemblies.Net.Net80.AddPackages([ + new PackageIdentity("Azure.Storage.Blobs", "12.17.0"), + new PackageIdentity("Azure.Storage.Queues", "12.17.0"), + new PackageIdentity("Azure.Data.Tables", "12.8.3"), + new PackageIdentity("Microsoft.Azure.Cosmos", "3.39.1"), + new PackageIdentity("Microsoft.Azure.Functions.Worker", "1.21.0"), + new PackageIdentity("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", "1.1.1"), + new PackageIdentity("Microsoft.Data.SqlClient", "5.2.0"), + ]); } From 2948bf40297370e4e133989967a77c549f06df38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 10:02:34 +0000 Subject: [PATCH 7/8] Fix code fixer not showing for TimeProvider diagnostics by finding parent InvocationExpressionSyntax Co-Authored-By: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../DateTimeOrchestrationFixer.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs index a7b3402f..8ddb1aaf 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Composition; using System.Globalization; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -68,14 +69,37 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday, isDateTimeOffset), equivalenceKey: title), // This key is used to prevent duplicate code fixes. context.Diagnostics); + return; } // Handle TimeProvider method invocations (e.g. TimeProvider.System.GetUtcNow()) - else if (orchestrationContext.SyntaxNodeWithDiagnostic is InvocationExpressionSyntax timeProviderInvocation && - semanticModel.GetSymbolInfo(timeProviderInvocation).Symbol is IMethodSymbol methodSymbol) + // The node might be the invocation itself or a child node, so we need to find the InvocationExpressionSyntax + InvocationExpressionSyntax? timeProviderInvocation = orchestrationContext.SyntaxNodeWithDiagnostic as InvocationExpressionSyntax + ?? orchestrationContext.SyntaxNodeWithDiagnostic.AncestorsAndSelf().OfType().FirstOrDefault(); + + if (timeProviderInvocation != null && + semanticModel.GetSymbolInfo(timeProviderInvocation).Symbol is IMethodSymbol methodSymbol) { string methodName = methodSymbol.Name; + // Check if this is one of the TimeProvider methods we handle + // The diagnostic was already reported by the analyzer, so we know it's valid + if (methodName is not ("GetUtcNow" or "GetLocalNow" or "GetTimestamp")) + { + return; + } + + // Optionally verify it's actually TimeProvider if the symbol is available + if (orchestrationContext.KnownTypeSymbols.TimeProvider != null) + { + bool isTimeProviderType = methodSymbol.ContainingType.Equals(orchestrationContext.KnownTypeSymbols.TimeProvider, SymbolEqualityComparer.Default); + if (!isTimeProviderType) + { + // This might be a different type with the same method name + return; + } + } + // Check if the method returns DateTimeOffset bool returnsDateTimeOffset = methodSymbol.ReturnType.ToDisplayString() == "System.DateTimeOffset"; From 68e3f101ca680bb90f1549213d07410c7c203218 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 31 Dec 2025 02:27:38 -0800 Subject: [PATCH 8/8] Revert "Make TimeProvider code fixer more lenient for conditional compilation scenarios" This reverts commit c48f1d85cad780fc334602ee68b7d3f3601fe4d7. --- .../DateTimeOrchestrationFixer.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs index 8ddb1aaf..221d6048 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs @@ -82,24 +82,6 @@ protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationC { string methodName = methodSymbol.Name; - // Check if this is one of the TimeProvider methods we handle - // The diagnostic was already reported by the analyzer, so we know it's valid - if (methodName is not ("GetUtcNow" or "GetLocalNow" or "GetTimestamp")) - { - return; - } - - // Optionally verify it's actually TimeProvider if the symbol is available - if (orchestrationContext.KnownTypeSymbols.TimeProvider != null) - { - bool isTimeProviderType = methodSymbol.ContainingType.Equals(orchestrationContext.KnownTypeSymbols.TimeProvider, SymbolEqualityComparer.Default); - if (!isTimeProviderType) - { - // This might be a different type with the same method name - return; - } - } - // Check if the method returns DateTimeOffset bool returnsDateTimeOffset = methodSymbol.ReturnType.ToDisplayString() == "System.DateTimeOffset";