Skip to content

Commit a8657fc

Browse files
authored
orchestration context roslyn code fixers (#323)
Following the same structure as #318, this PR adds roslyn code fixers for the diagnostics reported about non-deterministic usage of DateTime, Guid and Task delay. It suggests replacing them for the `TaskOrchestrationContext` alternatives, as suggested by [our docs](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints?tabs=csharp).
1 parent e358245 commit a8657fc

File tree

7 files changed

+534
-18
lines changed

7 files changed

+534
-18
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Immutable;
5+
using System.Composition;
6+
using System.Globalization;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CodeActions;
9+
using Microsoft.CodeAnalysis.CodeFixes;
10+
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
12+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
13+
14+
namespace Microsoft.DurableTask.Analyzers.Orchestration;
15+
16+
/// <summary>
17+
/// Code fix provider for the <see cref="DateTimeOrchestrationAnalyzer"/>.
18+
/// </summary>
19+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DateTimeOrchestrationFixer))]
20+
[Shared]
21+
public sealed class DateTimeOrchestrationFixer : OrchestrationContextFixer
22+
{
23+
/// <inheritdoc/>
24+
public override ImmutableArray<string> FixableDiagnosticIds => [DateTimeOrchestrationAnalyzer.DiagnosticId];
25+
26+
/// <inheritdoc/>
27+
protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext)
28+
{
29+
// Parses the syntax node to see if it is a member access expression (e.g. DateTime.Now)
30+
if (orchestrationContext.SyntaxNodeWithDiagnostic is not MemberAccessExpressionSyntax dateTimeExpression)
31+
{
32+
return;
33+
}
34+
35+
// Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx")
36+
string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name;
37+
38+
bool isDateTimeToday = dateTimeExpression.Name.ToString() == "Today";
39+
string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty;
40+
string recommendation = $"{contextParameterName}.CurrentUtcDateTime{dateTimeTodaySuffix}";
41+
42+
// e.g: "Use 'context.CurrentUtcDateTime' instead of 'DateTime.Now'"
43+
// e.g: "Use 'context.CurrentUtcDateTime.Date' instead of 'DateTime.Today'"
44+
string title = string.Format(
45+
CultureInfo.InvariantCulture,
46+
Resources.UseInsteadFixerTitle,
47+
recommendation,
48+
dateTimeExpression.ToString());
49+
50+
context.RegisterCodeFix(
51+
CodeAction.Create(
52+
title: title,
53+
createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday),
54+
equivalenceKey: title), // This key is used to prevent duplicate code fixes.
55+
context.Diagnostics);
56+
}
57+
58+
static Task<Document> ReplaceDateTime(Document document, SyntaxNode oldRoot, MemberAccessExpressionSyntax incorrectDateTimeSyntax, string contextParameterName, bool isDateTimeToday)
59+
{
60+
// Builds a 'context.CurrentUtcDateTime' syntax node
61+
MemberAccessExpressionSyntax correctDateTimeSyntax =
62+
MemberAccessExpression(
63+
SyntaxKind.SimpleMemberAccessExpression,
64+
IdentifierName(contextParameterName),
65+
IdentifierName("CurrentUtcDateTime"));
66+
67+
// If the original expression was DateTime.Today, we add ".Date" to the context expression.
68+
if (isDateTimeToday)
69+
{
70+
correctDateTimeSyntax = MemberAccessExpression(
71+
SyntaxKind.SimpleMemberAccessExpression,
72+
correctDateTimeSyntax,
73+
IdentifierName("Date"));
74+
}
75+
76+
// Replaces the old local declaration with the new local declaration.
77+
SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectDateTimeSyntax, correctDateTimeSyntax);
78+
Document newDocument = document.WithSyntaxRoot(newRoot);
79+
80+
return Task.FromResult(newDocument);
81+
}
82+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Immutable;
5+
using System.Composition;
6+
using System.Diagnostics;
7+
using System.Globalization;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CodeActions;
10+
using Microsoft.CodeAnalysis.CodeFixes;
11+
using Microsoft.CodeAnalysis.CSharp;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Operations;
14+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
15+
16+
namespace Microsoft.DurableTask.Analyzers.Orchestration;
17+
18+
/// <summary>
19+
/// Code fix provider for the <see cref="DelayOrchestrationAnalyzer"/>.
20+
/// </summary>
21+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DelayOrchestrationFixer))]
22+
[Shared]
23+
public sealed class DelayOrchestrationFixer : OrchestrationContextFixer
24+
{
25+
/// <inheritdoc/>
26+
public override ImmutableArray<string> FixableDiagnosticIds => [DelayOrchestrationAnalyzer.DiagnosticId];
27+
28+
/// <inheritdoc/>
29+
protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext)
30+
{
31+
if (orchestrationContext.SyntaxNodeWithDiagnostic is not InvocationExpressionSyntax invocationExpressionsSyntax)
32+
{
33+
return;
34+
}
35+
36+
if (orchestrationContext.SemanticModel.GetOperation(invocationExpressionsSyntax) is not IInvocationOperation invocationOperation)
37+
{
38+
return;
39+
}
40+
41+
// Only fix Task.Delay(int[,CancellationToken]) or Task.Delay(TimeSpan[,CancellationToken]) invocations.
42+
// For now, fixing Thread.Sleep(int) is not supported
43+
if (!SymbolEqualityComparer.Default.Equals(invocationOperation.Type, orchestrationContext.KnownTypeSymbols.Task))
44+
{
45+
return;
46+
}
47+
48+
Compilation compilation = orchestrationContext.SemanticModel.Compilation;
49+
INamedTypeSymbol int32 = compilation.GetSpecialType(SpecialType.System_Int32);
50+
51+
// Extracts the arguments from the Task.Delay invocation
52+
IMethodSymbol taskDelaySymbol = invocationOperation.TargetMethod;
53+
Debug.Assert(taskDelaySymbol.Parameters.Length >= 1, "Task.Delay should have at least one parameter");
54+
bool isInt = SymbolEqualityComparer.Default.Equals(taskDelaySymbol.Parameters[0].Type, int32);
55+
IArgumentOperation delayArgumentOperation = invocationOperation.Arguments[0];
56+
IArgumentOperation? cancellationTokenArgumentOperation = invocationOperation.Arguments.Length == 2 ? invocationOperation.Arguments[1] : null;
57+
58+
// Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx")
59+
string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name;
60+
string recommendation = $"{contextParameterName}.CreateTimer";
61+
62+
// e.g: "Use 'context.CreateTimer' instead of 'Task.Delay'"
63+
string title = string.Format(
64+
CultureInfo.InvariantCulture,
65+
Resources.UseInsteadFixerTitle,
66+
recommendation,
67+
"Task.Delay");
68+
69+
context.RegisterCodeFix(
70+
CodeAction.Create(
71+
title: title,
72+
createChangedDocument: c => ReplaceTaskDelay(
73+
context.Document, orchestrationContext.Root, invocationExpressionsSyntax, contextParameterName, delayArgumentOperation, cancellationTokenArgumentOperation, isInt),
74+
equivalenceKey: title), // This key is used to prevent duplicate code fixes.
75+
context.Diagnostics);
76+
}
77+
78+
static Task<Document> ReplaceTaskDelay(
79+
Document document,
80+
SyntaxNode oldRoot,
81+
InvocationExpressionSyntax incorrectTaskDelaySyntax,
82+
string contextParameterName,
83+
IArgumentOperation delayArgumentOperation,
84+
IArgumentOperation? cancellationTokenArgumentOperation,
85+
bool isInt)
86+
{
87+
if (delayArgumentOperation.Syntax is not ArgumentSyntax timeSpanOrIntArgumentSyntax)
88+
{
89+
return Task.FromResult(document);
90+
}
91+
92+
// Either use the original TimeSpan argument, or in case it is an int, transform it into TimeSpan
93+
ArgumentSyntax timeSpanArgumentSyntax;
94+
if (isInt)
95+
{
96+
timeSpanArgumentSyntax =
97+
Argument(
98+
InvocationExpression(
99+
MemberAccessExpression(
100+
SyntaxKind.SimpleMemberAccessExpression,
101+
IdentifierName("TimeSpan"),
102+
IdentifierName("FromMilliseconds")),
103+
ArgumentList(
104+
SeparatedList(new[] { timeSpanOrIntArgumentSyntax }))));
105+
}
106+
else
107+
{
108+
timeSpanArgumentSyntax = timeSpanOrIntArgumentSyntax;
109+
}
110+
111+
// Either gets the original cancellation token argument or create a 'CancellationToken.None'
112+
ArgumentSyntax cancellationTokenArgumentSyntax = cancellationTokenArgumentOperation?.Syntax as ArgumentSyntax ??
113+
Argument(
114+
MemberAccessExpression(
115+
SyntaxKind.SimpleMemberAccessExpression,
116+
IdentifierName("CancellationToken"),
117+
IdentifierName("None")));
118+
119+
// Builds a 'context.CreateTimer(TimeSpan.FromMilliseconds(1000), CancellationToken.None)' syntax node
120+
InvocationExpressionSyntax correctTimerSyntax =
121+
InvocationExpression(
122+
MemberAccessExpression(
123+
SyntaxKind.SimpleMemberAccessExpression,
124+
IdentifierName(contextParameterName),
125+
IdentifierName("CreateTimer")),
126+
ArgumentList(
127+
SeparatedList(new[]
128+
{
129+
timeSpanArgumentSyntax,
130+
cancellationTokenArgumentSyntax,
131+
})));
132+
133+
// Replaces the old local declaration with the new local declaration.
134+
SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectTaskDelaySyntax, correctTimerSyntax);
135+
Document newDocument = document.WithSyntaxRoot(newRoot);
136+
137+
return Task.FromResult(newDocument);
138+
}
139+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Immutable;
5+
using System.Composition;
6+
using System.Globalization;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CodeActions;
9+
using Microsoft.CodeAnalysis.CodeFixes;
10+
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
12+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
13+
14+
namespace Microsoft.DurableTask.Analyzers.Orchestration;
15+
16+
/// <summary>
17+
/// Code fix provider for the <see cref="GuidOrchestrationAnalyzer"/>.
18+
/// </summary>
19+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(GuidOrchestrationFixer))]
20+
[Shared]
21+
public sealed class GuidOrchestrationFixer : OrchestrationContextFixer
22+
{
23+
/// <inheritdoc/>
24+
public override ImmutableArray<string> FixableDiagnosticIds => [GuidOrchestrationAnalyzer.DiagnosticId];
25+
26+
/// <inheritdoc/>
27+
protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext)
28+
{
29+
// Parses the syntax node to see if it is a invocation expression (Guid.NewGuid())
30+
if (orchestrationContext.SyntaxNodeWithDiagnostic is not InvocationExpressionSyntax guidExpression)
31+
{
32+
return;
33+
}
34+
35+
// Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx")
36+
string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name;
37+
38+
string recommendation = $"{contextParameterName}.NewGuid()";
39+
40+
// e.g: "Use 'context.NewGuid()' instead of 'Guid.NewGuid()'"
41+
string title = string.Format(
42+
CultureInfo.InvariantCulture,
43+
Resources.UseInsteadFixerTitle,
44+
recommendation,
45+
guidExpression.ToString());
46+
47+
context.RegisterCodeFix(
48+
CodeAction.Create(
49+
title: title,
50+
createChangedDocument: c => ReplaceGuid(context.Document, orchestrationContext.Root, guidExpression, contextParameterName),
51+
equivalenceKey: title), // This key is used to prevent duplicate code fixes.
52+
context.Diagnostics);
53+
}
54+
55+
static Task<Document> ReplaceGuid(Document document, SyntaxNode oldRoot, InvocationExpressionSyntax incorrectGuidSyntax, string contextParameterName)
56+
{
57+
// Builds a 'context.NewGuid()' syntax node
58+
InvocationExpressionSyntax correctGuidSyntax =
59+
InvocationExpression(
60+
MemberAccessExpression(
61+
SyntaxKind.SimpleMemberAccessExpression,
62+
IdentifierName(contextParameterName),
63+
IdentifierName("NewGuid")),
64+
ArgumentList());
65+
66+
// Replaces the old local declaration with the new local declaration.
67+
SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectGuidSyntax, correctGuidSyntax);
68+
Document newDocument = document.WithSyntaxRoot(newRoot);
69+
70+
return Task.FromResult(newDocument);
71+
}
72+
}

0 commit comments

Comments
 (0)