Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 25 additions & 25 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
<PackageVersion Include="IntelliTect.Analyzers" Version="0.2.0" />
<PackageVersion Include="Microsoft.Build.Framework" Version="18.4.0" />
<PackageVersion Include="Microsoft.Build.Locator" Version="1.11.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.203" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="MSTest.TestAdapter" Version="4.2.2" />
<PackageVersion Include="MSTest.TestFramework" Version="4.2.2" />
<PackageVersion Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.25320.106" />
<!-- Reference-only packages for test framework namespace resolution in analyzer tests -->
<PackageVersion Include="xunit.v3.core" Version="3.2.2" />
<PackageVersion Include="NUnit" Version="4.6.0" />
<PackageVersion Include="TUnit.Core" Version="1.44.0" />
</ItemGroup>
</Project>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
<PackageVersion Include="IntelliTect.Analyzers" Version="0.2.0" />
<PackageVersion Include="Microsoft.Build.Framework" Version="18.4.0" />
<PackageVersion Include="Microsoft.Build.Locator" Version="1.11.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.203" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="MSTest.TestAdapter" Version="4.2.2" />
<PackageVersion Include="MSTest.TestFramework" Version="4.2.2" />
<PackageVersion Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.25320.106" />
<!-- Reference-only packages for test framework namespace resolution in analyzer tests -->
<PackageVersion Include="xunit.v3.core" Version="3.2.2" />
<PackageVersion Include="NUnit" Version="4.6.0" />
<PackageVersion Include="TUnit.Core" Version="1.44.0" />
</ItemGroup>
</Project>
198 changes: 198 additions & 0 deletions IntelliTect.Analyzer/IntelliTect.Analyzer.CodeFixes/TaskDelayZero.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;

namespace IntelliTect.Analyzer.CodeFixes
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TaskDelayZero))]
[Shared]
public class TaskDelayZero : CodeFixProvider
{
private const string Title = "Use Task.CompletedTask";

public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(Analyzers.TaskDelayZero.DiagnosticId);

public sealed override FixAllProvider GetFixAllProvider() =>
WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
{
return;
}

Diagnostic diagnostic = context.Diagnostics.First();
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;

InvocationExpressionSyntax? invocation = root.FindToken(diagnosticSpan.Start)
.Parent?.AncestorsAndSelf()
.OfType<InvocationExpressionSyntax>()
.FirstOrDefault();
if (invocation is null)
{
return;
}

SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel is null)
{
return;
}

if (semanticModel.GetOperation(invocation, context.CancellationToken) is not IInvocationOperation invocationOperation)
{
return;
}

ExpressionSyntax? replacement = CreateReplacementExpression(invocationOperation);
if (replacement is null)
{
return;
}

context.RegisterCodeFix(
CodeAction.Create(
title: Title,
createChangedDocument: c => ReplaceInvocationAsync(context.Document, invocation, replacement, c),
equivalenceKey: Title),
diagnostic);
}

private static ExpressionSyntax? CreateReplacementExpression(IInvocationOperation invocation)
{
if (!IntelliTect.Analyzer.Analyzers.TaskDelayZero.IsTaskDelayWithIntMilliseconds(invocation.TargetMethod))
{
return null;
}

IArgumentOperation? millisecondsDelayArgument = invocation.Arguments
.FirstOrDefault(a => a.Parameter?.Name == "millisecondsDelay");
if (millisecondsDelayArgument?.Value.ConstantValue is not { HasValue: true, Value: int millisecondsDelay }
|| millisecondsDelay != 0)
{
return null;
}

// Task.Delay overloads that include a TimeProvider cannot be safely rewritten to
// Task.CompletedTask/Task.FromCanceled without changing observable behavior.
if (invocation.Arguments.Any(a => a.Parameter?.Name == "timeProvider"))
{
return null;
}

IArgumentOperation? cancellationTokenArgument = invocation.Arguments
.FirstOrDefault(a => a.Parameter?.Name == "cancellationToken");
if (cancellationTokenArgument is null)
{
return CreateTaskCompletedTaskExpression();
}

if (cancellationTokenArgument.Value.Syntax is not ExpressionSyntax cancellationTokenExpression)
{
return null;
}

if (!IsSideEffectFree(cancellationTokenArgument.Value))
{
return null;
}

string tokenExpressionText = NormalizeCancellationTokenExpression(cancellationTokenExpression);
ExpressionSyntax normalizedTokenExpression = SyntaxFactory.ParseExpression(tokenExpressionText)
.WithAdditionalAnnotations(Simplifier.Annotation);

// Runtime behavior reference:
// https://github.com/dotnet/runtime/blob/1acc89c305165239a5a824567a3176b6b3342790/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L5907-L5911
// Task.Delay(0, token) maps to:
// token.IsCancellationRequested ? Task.FromCanceled(token) : Task.CompletedTask
return SyntaxFactory.ConditionalExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
normalizedTokenExpression,
SyntaxFactory.IdentifierName("IsCancellationRequested")),
CreateTaskFromCanceledExpression(normalizedTokenExpression),
CreateTaskCompletedTaskExpression());
}

private static ExpressionSyntax CreateTaskCompletedTaskExpression()
{
return SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.ParseName("global::System.Threading.Tasks.Task").WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.IdentifierName("CompletedTask"));
}

private static InvocationExpressionSyntax CreateTaskFromCanceledExpression(ExpressionSyntax cancellationTokenExpression)
{
return SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.ParseName("global::System.Threading.Tasks.Task").WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.IdentifierName("FromCanceled")),
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Argument(cancellationTokenExpression))));
}

private static string NormalizeCancellationTokenExpression(ExpressionSyntax cancellationTokenExpression)
{
return cancellationTokenExpression.Kind() switch
{
SyntaxKind.DefaultLiteralExpression => "default(global::System.Threading.CancellationToken)",
SyntaxKind.DefaultExpression => cancellationTokenExpression.WithoutTrivia().ToString() == "default"
? "default(global::System.Threading.CancellationToken)"
: cancellationTokenExpression.WithoutTrivia().ToString(),
_ => cancellationTokenExpression.WithoutTrivia().ToString()
};
}

private static bool IsSideEffectFree(IOperation operation)
{
return operation switch
{
ILocalReferenceOperation => true,
IParameterReferenceOperation => true,
IDefaultValueOperation => true,
IConversionOperation conversion => IsSideEffectFree(conversion.Operand),
IParenthesizedOperation parenthesized => IsSideEffectFree(parenthesized.Operand),
IPropertyReferenceOperation propertyReference
when propertyReference.Instance is null
&& propertyReference.Property.Name == "None"
&& propertyReference.Property.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
== "global::System.Threading.CancellationToken" => true,
_ => false
};
}

private static async Task<Document> ReplaceInvocationAsync(
Document document,
InvocationExpressionSyntax invocation,
ExpressionSyntax replacement,
CancellationToken cancellationToken)
{
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)
?? throw new System.InvalidOperationException("Could not get syntax root");

ExpressionSyntax replacementExpression = replacement
.WithTriviaFrom(invocation)
.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);

SyntaxNode newRoot = oldRoot.ReplaceNode(invocation, replacementExpression);
return document.WithSyntaxRoot(newRoot);
}
}
}
Loading
Loading