diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs new file mode 100644 index 00000000..5b32857c --- /dev/null +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Composition; +using System.Globalization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.DurableTask.Analyzers.Orchestration; + +/// +/// Code fix provider for the . +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DateTimeOrchestrationFixer))] +[Shared] +public sealed class DateTimeOrchestrationFixer : OrchestrationContextFixer +{ + /// + public override ImmutableArray FixableDiagnosticIds => [DateTimeOrchestrationAnalyzer.DiagnosticId]; + + /// + 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) + 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; + + 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'" + 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), + 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) + { + // Builds a 'context.CurrentUtcDateTime' syntax node + MemberAccessExpressionSyntax correctDateTimeSyntax = + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")); + + // If the original expression was DateTime.Today, we add ".Date" to the context expression. + if (isDateTimeToday) + { + correctDateTimeSyntax = MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + correctDateTimeSyntax, + IdentifierName("Date")); + } + + // Replaces the old local declaration with the new local declaration. + SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectDateTimeSyntax, correctDateTimeSyntax); + Document newDocument = document.WithSyntaxRoot(newRoot); + + return Task.FromResult(newDocument); + } +} diff --git a/src/Analyzers/Orchestration/DelayOrchestrationFixer.cs b/src/Analyzers/Orchestration/DelayOrchestrationFixer.cs new file mode 100644 index 00000000..18d85cc4 --- /dev/null +++ b/src/Analyzers/Orchestration/DelayOrchestrationFixer.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.Globalization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.DurableTask.Analyzers.Orchestration; + +/// +/// Code fix provider for the . +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DelayOrchestrationFixer))] +[Shared] +public sealed class DelayOrchestrationFixer : OrchestrationContextFixer +{ + /// + public override ImmutableArray FixableDiagnosticIds => [DelayOrchestrationAnalyzer.DiagnosticId]; + + /// + protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext) + { + if (orchestrationContext.SyntaxNodeWithDiagnostic is not InvocationExpressionSyntax invocationExpressionsSyntax) + { + return; + } + + if (orchestrationContext.SemanticModel.GetOperation(invocationExpressionsSyntax) is not IInvocationOperation invocationOperation) + { + return; + } + + // Only fix Task.Delay(int[,CancellationToken]) or Task.Delay(TimeSpan[,CancellationToken]) invocations. + // For now, fixing Thread.Sleep(int) is not supported + if (!SymbolEqualityComparer.Default.Equals(invocationOperation.Type, orchestrationContext.KnownTypeSymbols.Task)) + { + return; + } + + Compilation compilation = orchestrationContext.SemanticModel.Compilation; + INamedTypeSymbol int32 = compilation.GetSpecialType(SpecialType.System_Int32); + + // Extracts the arguments from the Task.Delay invocation + IMethodSymbol taskDelaySymbol = invocationOperation.TargetMethod; + Debug.Assert(taskDelaySymbol.Parameters.Length >= 1, "Task.Delay should have at least one parameter"); + bool isInt = SymbolEqualityComparer.Default.Equals(taskDelaySymbol.Parameters[0].Type, int32); + IArgumentOperation delayArgumentOperation = invocationOperation.Arguments[0]; + IArgumentOperation? cancellationTokenArgumentOperation = invocationOperation.Arguments.Length == 2 ? invocationOperation.Arguments[1] : null; + + // Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx") + string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name; + string recommendation = $"{contextParameterName}.CreateTimer"; + + // e.g: "Use 'context.CreateTimer' instead of 'Task.Delay'" + string title = string.Format( + CultureInfo.InvariantCulture, + Resources.UseInsteadFixerTitle, + recommendation, + "Task.Delay"); + + context.RegisterCodeFix( + CodeAction.Create( + title: title, + createChangedDocument: c => ReplaceTaskDelay( + context.Document, orchestrationContext.Root, invocationExpressionsSyntax, contextParameterName, delayArgumentOperation, cancellationTokenArgumentOperation, isInt), + equivalenceKey: title), // This key is used to prevent duplicate code fixes. + context.Diagnostics); + } + + static Task ReplaceTaskDelay( + Document document, + SyntaxNode oldRoot, + InvocationExpressionSyntax incorrectTaskDelaySyntax, + string contextParameterName, + IArgumentOperation delayArgumentOperation, + IArgumentOperation? cancellationTokenArgumentOperation, + bool isInt) + { + if (delayArgumentOperation.Syntax is not ArgumentSyntax timeSpanOrIntArgumentSyntax) + { + return Task.FromResult(document); + } + + // Either use the original TimeSpan argument, or in case it is an int, transform it into TimeSpan + ArgumentSyntax timeSpanArgumentSyntax; + if (isInt) + { + timeSpanArgumentSyntax = + Argument( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("TimeSpan"), + IdentifierName("FromMilliseconds")), + ArgumentList( + SeparatedList(new[] { timeSpanOrIntArgumentSyntax })))); + } + else + { + timeSpanArgumentSyntax = timeSpanOrIntArgumentSyntax; + } + + // Either gets the original cancellation token argument or create a 'CancellationToken.None' + ArgumentSyntax cancellationTokenArgumentSyntax = cancellationTokenArgumentOperation?.Syntax as ArgumentSyntax ?? + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("CancellationToken"), + IdentifierName("None"))); + + // Builds a 'context.CreateTimer(TimeSpan.FromMilliseconds(1000), CancellationToken.None)' syntax node + InvocationExpressionSyntax correctTimerSyntax = + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CreateTimer")), + ArgumentList( + SeparatedList(new[] + { + timeSpanArgumentSyntax, + cancellationTokenArgumentSyntax, + }))); + + // Replaces the old local declaration with the new local declaration. + SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectTaskDelaySyntax, correctTimerSyntax); + Document newDocument = document.WithSyntaxRoot(newRoot); + + return Task.FromResult(newDocument); + } +} diff --git a/src/Analyzers/Orchestration/GuidOrchestrationFixer.cs b/src/Analyzers/Orchestration/GuidOrchestrationFixer.cs new file mode 100644 index 00000000..139bd8ff --- /dev/null +++ b/src/Analyzers/Orchestration/GuidOrchestrationFixer.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Composition; +using System.Globalization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.DurableTask.Analyzers.Orchestration; + +/// +/// Code fix provider for the . +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(GuidOrchestrationFixer))] +[Shared] +public sealed class GuidOrchestrationFixer : OrchestrationContextFixer +{ + /// + public override ImmutableArray FixableDiagnosticIds => [GuidOrchestrationAnalyzer.DiagnosticId]; + + /// + protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext) + { + // Parses the syntax node to see if it is a invocation expression (Guid.NewGuid()) + if (orchestrationContext.SyntaxNodeWithDiagnostic is not InvocationExpressionSyntax guidExpression) + { + return; + } + + // Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx") + string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name; + + string recommendation = $"{contextParameterName}.NewGuid()"; + + // e.g: "Use 'context.NewGuid()' instead of 'Guid.NewGuid()'" + string title = string.Format( + CultureInfo.InvariantCulture, + Resources.UseInsteadFixerTitle, + recommendation, + guidExpression.ToString()); + + context.RegisterCodeFix( + CodeAction.Create( + title: title, + createChangedDocument: c => ReplaceGuid(context.Document, orchestrationContext.Root, guidExpression, contextParameterName), + equivalenceKey: title), // This key is used to prevent duplicate code fixes. + context.Diagnostics); + } + + static Task ReplaceGuid(Document document, SyntaxNode oldRoot, InvocationExpressionSyntax incorrectGuidSyntax, string contextParameterName) + { + // Builds a 'context.NewGuid()' syntax node + InvocationExpressionSyntax correctGuidSyntax = + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("NewGuid")), + ArgumentList()); + + // Replaces the old local declaration with the new local declaration. + SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectGuidSyntax, correctGuidSyntax); + Document newDocument = document.WithSyntaxRoot(newRoot); + + return Task.FromResult(newDocument); + } +} diff --git a/src/Analyzers/Orchestration/OrchestrationContextFixer.cs b/src/Analyzers/Orchestration/OrchestrationContextFixer.cs new file mode 100644 index 00000000..50097d21 --- /dev/null +++ b/src/Analyzers/Orchestration/OrchestrationContextFixer.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.DurableTask.Analyzers.Orchestration; + +/// +/// Context information for the . +/// +/// The Semantic Model retrieved from the Document. +/// Well-known types that are used by the Durable analyzers. +/// The root Syntax Node retrieved from the Document. +/// Syntax Node that contains the diagnostic. +/// The 'TaskOrchestrationContext' symbol. +public readonly struct OrchestrationCodeFixContext( + SemanticModel semanticModel, + KnownTypeSymbols knownTypeSymbols, + SyntaxNode root, + SyntaxNode syntaxNodeWithDiagnostic, + IParameterSymbol taskOrchestrationContextSymbol) +{ + /// + /// Gets the Semantic Model retrieved from the Document. + /// + public SemanticModel SemanticModel { get; } = semanticModel; + + /// + /// Gets the well-known types that are used by the Durable analyzers. + /// + public KnownTypeSymbols KnownTypeSymbols { get; } = knownTypeSymbols; + + /// + /// Gets the root Syntax Node retrieved from the Document. + /// + public SyntaxNode Root { get; } = root; + + /// + /// Gets the Syntax Node that contains the diagnostic. + /// + public SyntaxNode SyntaxNodeWithDiagnostic { get; } = syntaxNodeWithDiagnostic; + + /// + /// Gets the 'TaskOrchestrationContext' symbol. + /// + public IParameterSymbol TaskOrchestrationContextSymbol { get; } = taskOrchestrationContextSymbol; +} + +/// +/// Base class for code fix providers that fix issues in orchestrator methods by replacing a SyntaxNode with a TaskOrchestrationContext member or invocation. +/// +public abstract class OrchestrationContextFixer : CodeFixProvider +{ + /// + public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); + if (root == null) + { + return; + } + + // Find the Syntax Node that is causing the diagnostic. + if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode syntaxNodeWithDiagnostic) + { + return; + } + + SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken); + if (semanticModel == null) + { + return; + } + + // Analyze the data flow to determine which variables are assigned (reachable) within the scope. + DataFlowAnalysis? dataFlowAnalysis = semanticModel.AnalyzeDataFlow(syntaxNodeWithDiagnostic); + if (dataFlowAnalysis == null) + { + return; + } + + KnownTypeSymbols knownTypeSymbols = new(semanticModel.Compilation); + if (knownTypeSymbols.TaskOrchestrationContext == null) + { + return; + } + + // Find the TaskOrchestrationContext parameter available in the scope. + IParameterSymbol? taskOrchestrationContextSymbol = dataFlowAnalysis.DefinitelyAssignedOnEntry + .OfType() + .FirstOrDefault( + p => p.Type.Equals(knownTypeSymbols.TaskOrchestrationContext, SymbolEqualityComparer.Default)); + if (taskOrchestrationContextSymbol == null) + { + // This method does not have a TaskOrchestrationContext parameter, so we should not offer this code fix. + return; + } + + var orchestrationContext = new OrchestrationCodeFixContext( + semanticModel, knownTypeSymbols, root, syntaxNodeWithDiagnostic, taskOrchestrationContextSymbol); + + this.RegisterCodeFixes(context, orchestrationContext); + } + + /// + /// Registers a code fix for an orchestration diagnostic that can be fixed by replacing a SyntaxNode with a TaskOrchestrationContext member or invocation. + /// + /// A containing context information about the diagnostics to fix. + /// A containing context information about the orchestration code fixer. + protected abstract void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext); +} diff --git a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs index 18a90349..b9ae81e5 100644 --- a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs +++ b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs @@ -4,7 +4,7 @@ using Microsoft.CodeAnalysis.Testing; using Microsoft.DurableTask.Analyzers.Orchestration; -using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; +using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpCodeFixVerifier; namespace Microsoft.DurableTask.Analyzers.Tests.Orchestration; @@ -45,7 +45,6 @@ void Method(){ [Theory] [InlineData("DateTime.Now")] [InlineData("DateTime.UtcNow")] - [InlineData("DateTime.Today")] public async Task DurableFunctionOrchestrationUsingDateTimeNonDeterministicPropertiesHasDiag(string expression) { string code = Wrapper.WrapDurableFunctionOrchestration($@" @@ -54,11 +53,43 @@ DateTime Run([OrchestrationTrigger] TaskOrchestrationContext context) {{ return {{|#0:{expression}|}}; }} +"); + + string fix = Wrapper.WrapDurableFunctionOrchestration($@" +[Function(""Run"")] +DateTime Run([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return context.CurrentUtcDateTime; +}} "); DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", $"System.{expression}", "Run"); - await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task DurableFunctionOrchestrationUsingDateTimeTodayHasDiag() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +DateTime Run([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return {|#0:DateTime.Today|}; +} +"); + + string fix = Wrapper.WrapDurableFunctionOrchestration($@" +[Function(""Run"")] +DateTime Run([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return context.CurrentUtcDateTime.Date; +}} +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", $"System.DateTime.Today", "Run"); + + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } [Fact] @@ -187,16 +218,24 @@ public class MyOrchestrator : TaskOrchestrator { public override Task RunAsync(TaskOrchestrationContext context, string input) { - return Task.FromResult(Method()); + return Task.FromResult({|#0:DateTime.Now|}); } +} +"); - private DateTime Method() => {|#0:DateTime.Now|}; + string fix = Wrapper.WrapTaskOrchestrator(@" +public class MyOrchestrator : TaskOrchestrator +{ + public override Task RunAsync(TaskOrchestrationContext context, string input) + { + return Task.FromResult(context.CurrentUtcDateTime); + } } "); - DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Method", "System.DateTime.Now", "MyOrchestrator"); + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.DateTime.Now", "MyOrchestrator"); - await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } [Fact] @@ -257,11 +296,18 @@ public async Task FuncOrchestratorWithLambdaHasDiag() { return {|#0:DateTime.Now|}; }); +"); + + string fix = Wrapper.WrapFuncOrchestrator(@" +tasks.AddOrchestratorFunc(""HelloSequence"", context => +{ + return context.CurrentUtcDateTime; +}); "); DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.DateTime.Now", "HelloSequence"); - await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } [Fact] @@ -291,11 +337,37 @@ static DateTime MyRunAsync(TaskOrchestrationContext context) return {|#0:DateTime.Now|}; } } +"; + + string fix = @" +using System; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; + +public class Program +{ + public static void Main() + { + new ServiceCollection().AddDurableTaskWorker(builder => + { + builder.AddTasks(tasks => + { + tasks.AddOrchestratorFunc(""MyRun"", MyRunAsync); + }); + }); + } + + static DateTime MyRunAsync(TaskOrchestrationContext context) + { + return context.CurrentUtcDateTime; + } +} "; DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("MyRunAsync", "System.DateTime.Now", "MyRun"); - await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } diff --git a/test/Analyzers.Tests/Orchestration/DelayOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/DelayOrchestrationAnalyzerTests.cs index 39053224..6f1b9151 100644 --- a/test/Analyzers.Tests/Orchestration/DelayOrchestrationAnalyzerTests.cs +++ b/test/Analyzers.Tests/Orchestration/DelayOrchestrationAnalyzerTests.cs @@ -3,8 +3,7 @@ using Microsoft.CodeAnalysis.Testing; using Microsoft.DurableTask.Analyzers.Orchestration; - -using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; +using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpCodeFixVerifier; namespace Microsoft.DurableTask.Analyzers.Tests.Orchestration; @@ -40,12 +39,23 @@ public async Task DurableFunctionOrchestrationUsingTaskDelayHasDiag() [Function(""Run"")] async Task Method([OrchestrationTrigger] TaskOrchestrationContext context) { - await {|#0:Task.Delay(1000)|}; + CancellationToken t = CancellationToken.None; + await {|#0:Task.Delay(1000, t)|}; } "); - DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Method", "Task.Delay(int)", "Run"); - await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + string fix = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +async Task Method([OrchestrationTrigger] TaskOrchestrationContext context) +{ + CancellationToken t = CancellationToken.None; + await context.CreateTimer(TimeSpan.FromMilliseconds(1000), t); +} +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Method", "Task.Delay(int, CancellationToken)", "Run"); + + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } [Fact] diff --git a/test/Analyzers.Tests/Orchestration/GuidOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/GuidOrchestrationAnalyzerTests.cs index 838a81d1..335f02f9 100644 --- a/test/Analyzers.Tests/Orchestration/GuidOrchestrationAnalyzerTests.cs +++ b/test/Analyzers.Tests/Orchestration/GuidOrchestrationAnalyzerTests.cs @@ -4,7 +4,7 @@ using Microsoft.CodeAnalysis.Testing; using Microsoft.DurableTask.Analyzers.Orchestration; -using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; +using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpCodeFixVerifier; namespace Microsoft.DurableTask.Analyzers.Tests.Orchestration; @@ -27,11 +27,19 @@ Guid Method([OrchestrationTrigger] TaskOrchestrationContext context) { return {|#0:Guid.NewGuid()|}; } +"); + + string fix = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +Guid Method([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return context.NewGuid(); +} "); DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Method", "Guid.NewGuid()", "Run"); - await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } [Fact] @@ -45,11 +53,21 @@ public override Task RunAsync(TaskOrchestrationContext context, string inp return Task.FromResult({|#0:Guid.NewGuid()|}); } } +"); + + string fix = Wrapper.WrapTaskOrchestrator(@" +public class MyOrchestrator : TaskOrchestrator +{ + public override Task RunAsync(TaskOrchestrationContext context, string input) + { + return Task.FromResult(context.NewGuid()); + } +} "); DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "Guid.NewGuid()", "MyOrchestrator"); - await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } [Fact] @@ -60,11 +78,18 @@ public async Task FuncOrchestratorUsingNewGuidHasDiag() { return {|#0:Guid.NewGuid()|}; }); +"); + + string fix = Wrapper.WrapFuncOrchestrator(@" +tasks.AddOrchestratorFunc(""HelloSequence"", context => +{ + return context.NewGuid(); +}); "); DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "Guid.NewGuid()", "HelloSequence"); - await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } static DiagnosticResult BuildDiagnostic()