Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DateTime, Guid and Task Delay roslyn code fixers #323

Merged
Merged
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
82 changes: 82 additions & 0 deletions src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Code fix provider for the <see cref="DateTimeOrchestrationAnalyzer"/>.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DateTimeOrchestrationFixer))]
[Shared]
public sealed class DateTimeOrchestrationFixer : OrchestrationContextFixer
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [DateTimeOrchestrationAnalyzer.DiagnosticId];

/// <inheritdoc/>
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<Document> 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);
}
}
139 changes: 139 additions & 0 deletions src/Analyzers/Orchestration/DelayOrchestrationFixer.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Code fix provider for the <see cref="DelayOrchestrationAnalyzer"/>.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DelayOrchestrationFixer))]
[Shared]
public sealed class DelayOrchestrationFixer : OrchestrationContextFixer
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [DelayOrchestrationAnalyzer.DiagnosticId];

/// <inheritdoc/>
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<Document> 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);
}
}
72 changes: 72 additions & 0 deletions src/Analyzers/Orchestration/GuidOrchestrationFixer.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Code fix provider for the <see cref="GuidOrchestrationAnalyzer"/>.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(GuidOrchestrationFixer))]
[Shared]
public sealed class GuidOrchestrationFixer : OrchestrationContextFixer
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [GuidOrchestrationAnalyzer.DiagnosticId];

/// <inheritdoc/>
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<Document> 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);
}
}
Loading
Loading