Skip to content

Commit ed2cb5d

Browse files
Address PR review feedback for INTL0304 code fix
- Add Simplifier annotations so code-fix output simplifies Task symbols - Build replacement expressions with syntax nodes instead of raw strings - Skip code-fix rewrites when Task.Delay overload includes timeProvider - Add regression tests for TimeProvider overload no-fix behavior - Update fixed-code expectations for simplified output
1 parent b3c18a2 commit ed2cb5d

2 files changed

Lines changed: 83 additions & 9 deletions

File tree

IntelliTect.Analyzer/IntelliTect.Analyzer.CodeFixes/TaskDelayZero.cs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.CodeAnalysis.CSharp.Syntax;
1111
using Microsoft.CodeAnalysis.Formatting;
1212
using Microsoft.CodeAnalysis.Operations;
13+
using Microsoft.CodeAnalysis.Simplification;
1314
using Microsoft.CodeAnalysis.Text;
1415

1516
namespace IntelliTect.Analyzer.CodeFixes
@@ -86,11 +87,18 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
8687
return null;
8788
}
8889

90+
// Task.Delay overloads that include a TimeProvider cannot be safely rewritten to
91+
// Task.CompletedTask/Task.FromCanceled without changing observable behavior.
92+
if (invocation.Arguments.Any(a => a.Parameter?.Name == "timeProvider"))
93+
{
94+
return null;
95+
}
96+
8997
IArgumentOperation? cancellationTokenArgument = invocation.Arguments
9098
.FirstOrDefault(a => a.Parameter?.Name == "cancellationToken");
9199
if (cancellationTokenArgument is null)
92100
{
93-
return SyntaxFactory.ParseExpression("global::System.Threading.Tasks.Task.CompletedTask");
101+
return CreateTaskCompletedTaskExpression();
94102
}
95103

96104
if (cancellationTokenArgument.Value.Syntax is not ExpressionSyntax cancellationTokenExpression)
@@ -104,13 +112,40 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
104112
}
105113

106114
string tokenExpressionText = NormalizeCancellationTokenExpression(cancellationTokenExpression);
115+
ExpressionSyntax normalizedTokenExpression = SyntaxFactory.ParseExpression(tokenExpressionText)
116+
.WithAdditionalAnnotations(Simplifier.Annotation);
107117

108118
// Runtime behavior reference:
109119
// https://github.com/dotnet/runtime/blob/1acc89c305165239a5a824567a3176b6b3342790/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L5907-L5911
110120
// Task.Delay(0, token) maps to:
111121
// token.IsCancellationRequested ? Task.FromCanceled(token) : Task.CompletedTask
112-
return SyntaxFactory.ParseExpression(
113-
$"({tokenExpressionText}.IsCancellationRequested ? global::System.Threading.Tasks.Task.FromCanceled({tokenExpressionText}) : global::System.Threading.Tasks.Task.CompletedTask)");
122+
return SyntaxFactory.ConditionalExpression(
123+
SyntaxFactory.MemberAccessExpression(
124+
SyntaxKind.SimpleMemberAccessExpression,
125+
normalizedTokenExpression,
126+
SyntaxFactory.IdentifierName("IsCancellationRequested")),
127+
CreateTaskFromCanceledExpression(normalizedTokenExpression),
128+
CreateTaskCompletedTaskExpression());
129+
}
130+
131+
private static ExpressionSyntax CreateTaskCompletedTaskExpression()
132+
{
133+
return SyntaxFactory.MemberAccessExpression(
134+
SyntaxKind.SimpleMemberAccessExpression,
135+
SyntaxFactory.ParseName("global::System.Threading.Tasks.Task").WithAdditionalAnnotations(Simplifier.Annotation),
136+
SyntaxFactory.IdentifierName("CompletedTask"));
137+
}
138+
139+
private static InvocationExpressionSyntax CreateTaskFromCanceledExpression(ExpressionSyntax cancellationTokenExpression)
140+
{
141+
return SyntaxFactory.InvocationExpression(
142+
SyntaxFactory.MemberAccessExpression(
143+
SyntaxKind.SimpleMemberAccessExpression,
144+
SyntaxFactory.ParseName("global::System.Threading.Tasks.Task").WithAdditionalAnnotations(Simplifier.Annotation),
145+
SyntaxFactory.IdentifierName("FromCanceled")),
146+
SyntaxFactory.ArgumentList(
147+
SyntaxFactory.SingletonSeparatedList(
148+
SyntaxFactory.Argument(cancellationTokenExpression))));
114149
}
115150

116151
private static string NormalizeCancellationTokenExpression(ExpressionSyntax cancellationTokenExpression)
@@ -154,7 +189,7 @@ private static async Task<Document> ReplaceInvocationAsync(
154189

155190
ExpressionSyntax replacementExpression = replacement
156191
.WithTriviaFrom(invocation)
157-
.WithAdditionalAnnotations(Formatter.Annotation);
192+
.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
158193

159194
SyntaxNode newRoot = oldRoot.ReplaceNode(invocation, replacementExpression);
160195
return document.WithSyntaxRoot(newRoot);

IntelliTect.Analyzer/IntelliTect.Analyzer.Test/TaskDelayZeroTests.cs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class C
109109
{
110110
Task M()
111111
{
112-
return global::System.Threading.Tasks.Task.CompletedTask;
112+
return Task.CompletedTask;
113113
}
114114
}
115115
""";
@@ -141,7 +141,7 @@ class C
141141
{
142142
Task M(CancellationToken token)
143143
{
144-
return (token.IsCancellationRequested ? global::System.Threading.Tasks.Task.FromCanceled(token) : global::System.Threading.Tasks.Task.CompletedTask);
144+
return token.IsCancellationRequested ? Task.FromCanceled(token) : Task.CompletedTask;
145145
}
146146
}
147147
""";
@@ -171,7 +171,7 @@ class C
171171
{
172172
Task M()
173173
{
174-
return (default(global::System.Threading.CancellationToken).IsCancellationRequested ? global::System.Threading.Tasks.Task.FromCanceled(default(global::System.Threading.CancellationToken)) : global::System.Threading.Tasks.Task.CompletedTask);
174+
return default(global::System.Threading.CancellationToken).IsCancellationRequested ? Task.FromCanceled(default) : Task.CompletedTask;
175175
}
176176
}
177177
""";
@@ -245,14 +245,53 @@ class C
245245
{
246246
Task M()
247247
{
248-
return (CancellationToken.None.IsCancellationRequested ? global::System.Threading.Tasks.Task.FromCanceled(CancellationToken.None) : global::System.Threading.Tasks.Task.CompletedTask);
248+
return CancellationToken.None.IsCancellationRequested ? Task.FromCanceled(CancellationToken.None) : Task.CompletedTask;
249249
}
250250
}
251251
""";
252252

253253
await VerifyCSharpFix(source, fixedSource);
254254
}
255255

256+
[TestMethod]
257+
public async Task TaskDelayZeroWithTimeProvider_NoCodeFix()
258+
{
259+
string source = """
260+
using System;
261+
using System.Threading.Tasks;
262+
263+
class C
264+
{
265+
Task M()
266+
{
267+
return Task.Delay(0, TimeProvider.System);
268+
}
269+
}
270+
""";
271+
272+
await VerifyCSharpFix(source, source);
273+
}
274+
275+
[TestMethod]
276+
public async Task TaskDelayZeroWithTimeProviderAndToken_NoCodeFix()
277+
{
278+
string source = """
279+
using System;
280+
using System.Threading;
281+
using System.Threading.Tasks;
282+
283+
class C
284+
{
285+
Task M(CancellationToken token)
286+
{
287+
return Task.Delay(0, TimeProvider.System, token);
288+
}
289+
}
290+
""";
291+
292+
await VerifyCSharpFix(source, source);
293+
}
294+
256295
[TestMethod]
257296
public async Task AwaitTaskDelayZero_CodeFix_UsesCompletedTask()
258297
{
@@ -275,7 +314,7 @@ class C
275314
{
276315
async Task M()
277316
{
278-
await global::System.Threading.Tasks.Task.CompletedTask;
317+
await Task.CompletedTask;
279318
}
280319
}
281320
""";

0 commit comments

Comments
 (0)