-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
428 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
src/EcoCode.Core/Analyzers/EC93.ReturnTaskDirectly.Fixer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
namespace EcoCode.Analyzers; | ||
|
||
/// <summary>EC93 fixer: Return Task directly.</summary> | ||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ReturnTaskDirectly)), Shared] | ||
public sealed class ReturnTaskDirectlyFixer : CodeFixProvider | ||
{ | ||
/// <inheritdoc/> | ||
public override ImmutableArray<string> FixableDiagnosticIds => _fixableDiagnosticIds; | ||
private static readonly ImmutableArray<string> _fixableDiagnosticIds = [ReturnTaskDirectly.Descriptor.Id]; | ||
|
||
/// <inheritdoc/> | ||
[ExcludeFromCodeCoverage] | ||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||
|
||
/// <inheritdoc/> | ||
public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root) | ||
return; | ||
|
||
foreach (var diagnostic in context.Diagnostics) | ||
{ | ||
if (root.FindToken(diagnostic.Location.SourceSpan.Start).Parent is not { } parent) | ||
continue; | ||
|
||
foreach (var node in parent.AncestorsAndSelf()) | ||
{ | ||
if (node is not MethodDeclarationSyntax methodDecl) continue; | ||
|
||
int asyncIndex = methodDecl.Modifiers.IndexOf(SyntaxKind.AsyncKeyword); | ||
if (asyncIndex == -1) continue; | ||
|
||
if (methodDecl.ExpressionBody is { Expression: AwaitExpressionSyntax awaitExpr1 }) | ||
{ | ||
context.RegisterCodeFix( // Expression body | ||
CodeAction.Create( | ||
title: "Dispose resource asynchronously", | ||
createChangedDocument: _ => ReturnTaskDirectlyWithExpressionAsync(context.Document, methodDecl, awaitExpr1, asyncIndex), | ||
equivalenceKey: "Dispose resource asynchronously"), | ||
diagnostic); | ||
break; | ||
} | ||
|
||
var statement = methodDecl.Body?.Statements.SingleOrDefaultNoThrow(); | ||
if (statement is ExpressionStatementSyntax { Expression: AwaitExpressionSyntax awaitExpr2 }) | ||
{ | ||
context.RegisterCodeFix( // Body with 'await' statement | ||
CodeAction.Create( | ||
title: "Dispose resource asynchronously", | ||
createChangedDocument: _ => ReturnTaskDirectlyWithBodyAwaitAsync(context.Document, methodDecl, awaitExpr2, asyncIndex), | ||
equivalenceKey: "Dispose resource asynchronously"), | ||
diagnostic); | ||
break; | ||
} | ||
if (statement is ReturnStatementSyntax { Expression: AwaitExpressionSyntax awaitExpr3 } returnStmt) | ||
{ | ||
context.RegisterCodeFix( // Body with 'return await' statement | ||
CodeAction.Create( | ||
title: "Dispose resource asynchronously", | ||
createChangedDocument: _ => ReturnTaskDirectlyWithBodyReturnAwaitAsync(context.Document, methodDecl, returnStmt, awaitExpr3, asyncIndex), | ||
equivalenceKey: "Dispose resource asynchronously"), | ||
diagnostic); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
private static async Task<Document> ReturnTaskDirectlyWithExpressionAsync( | ||
Document document, | ||
MethodDeclarationSyntax methodDecl, | ||
AwaitExpressionSyntax awaitExpr, | ||
int asyncIndex) | ||
{ | ||
var newReturnStmt = SyntaxFactory.ReturnStatement(awaitExpr.Expression); | ||
|
||
var newBody = SyntaxFactory.ArrowExpressionClause(newReturnStmt.Expression!.WithTriviaFrom(awaitExpr)) | ||
.WithTriviaFrom(methodDecl.ExpressionBody!); | ||
|
||
return await document.WithUpdatedRoot(methodDecl, methodDecl | ||
.WithModifiers(methodDecl.Modifiers.RemoveAt(asyncIndex)) | ||
.WithExpressionBody(newBody)); | ||
} | ||
|
||
private static async Task<Document> ReturnTaskDirectlyWithBodyAwaitAsync( | ||
Document document, | ||
MethodDeclarationSyntax methodDecl, | ||
AwaitExpressionSyntax awaitExpr, | ||
int asyncIndex) | ||
{ | ||
var newReturnStmt = SyntaxFactory.ReturnStatement(awaitExpr.Expression) | ||
.WithLeadingTrivia(awaitExpr.GetLeadingTrivia()) | ||
.WithTrailingTrivia(((ExpressionStatementSyntax)awaitExpr.Parent!).SemicolonToken.TrailingTrivia); | ||
|
||
var newBody = SyntaxFactory.Block(newReturnStmt) | ||
.WithOpenBraceToken(methodDecl.Body!.OpenBraceToken) | ||
.WithCloseBraceToken(methodDecl.Body.CloseBraceToken) | ||
.WithTriviaFrom(methodDecl.Body); | ||
|
||
return await document.WithUpdatedRoot(methodDecl, methodDecl | ||
.WithModifiers(methodDecl.Modifiers.RemoveAt(asyncIndex)) | ||
.WithBody(newBody)); | ||
} | ||
|
||
private static async Task<Document> ReturnTaskDirectlyWithBodyReturnAwaitAsync( | ||
Document document, | ||
MethodDeclarationSyntax methodDecl, | ||
ReturnStatementSyntax returnStmt, | ||
AwaitExpressionSyntax awaitExpr, | ||
int asyncIndex) | ||
{ | ||
var newReturnStmt = returnStmt.WithExpression(awaitExpr.Expression); | ||
|
||
var newBody = SyntaxFactory.Block(newReturnStmt) | ||
.WithOpenBraceToken(methodDecl.Body!.OpenBraceToken) | ||
.WithCloseBraceToken(methodDecl.Body.CloseBraceToken) | ||
.WithTriviaFrom(methodDecl.Body); | ||
|
||
return await document.WithUpdatedRoot(methodDecl, methodDecl | ||
.WithModifiers(methodDecl.Modifiers.RemoveAt(asyncIndex)) | ||
.WithBody(newBody)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
namespace EcoCode.Analyzers; | ||
|
||
/// <summary>EC93: Return Task directly.</summary> | ||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public sealed class ReturnTaskDirectly : DiagnosticAnalyzer | ||
{ | ||
private static readonly ImmutableArray<SyntaxKind> MethodDeclarations = [SyntaxKind.MethodDeclaration]; | ||
|
||
/// <summary>The diagnostic descriptor.</summary> | ||
public static DiagnosticDescriptor Descriptor { get; } = Rule.CreateDescriptor( | ||
id: Rule.Ids.EC93_ReturnTaskDirectly, | ||
title: "Consider returning Task directly", | ||
message: "Consider returning a Task directly instead of awaiting a single statement", | ||
category: Rule.Categories.Performance, | ||
severity: DiagnosticSeverity.Info, | ||
description: "Consider returning a Task directly instead of awaiting a single statement, as this can save performance."); | ||
|
||
/// <inheritdoc/> | ||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => _supportedDiagnostics; | ||
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics = [Descriptor]; | ||
|
||
/// <inheritdoc/> | ||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.EnableConcurrentExecution(); | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.RegisterSyntaxNodeAction(static context => AnalyzeSyntaxNode(context), MethodDeclarations); | ||
} | ||
|
||
private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) | ||
{ | ||
// Check if the method is async | ||
var methodDeclaration = (MethodDeclarationSyntax)context.Node; | ||
int asyncIndex = methodDeclaration.Modifiers.IndexOf(SyntaxKind.AsyncKeyword); | ||
if (asyncIndex == -1) return; | ||
|
||
// Check if the method contains a single await statement | ||
var awaitExpr = methodDeclaration.ExpressionBody?.Expression as AwaitExpressionSyntax; | ||
if (awaitExpr is null && methodDeclaration.Body?.Statements.SingleOrDefaultNoThrow() is { } statement) | ||
{ | ||
if (statement is ExpressionStatementSyntax expressionStmt) // Is it an 'await' statement | ||
awaitExpr = expressionStmt.Expression as AwaitExpressionSyntax; | ||
else if (statement is ReturnStatementSyntax returnStmt) // Is it a 'return await' statement | ||
awaitExpr = returnStmt.Expression as AwaitExpressionSyntax; | ||
} | ||
if (awaitExpr is null) return; | ||
|
||
// Check if the await statement has any nested await statement (like parameters) | ||
foreach (var node in awaitExpr.DescendantNodes()) | ||
if (node is AwaitExpressionSyntax) return; | ||
|
||
context.ReportDiagnostic(Diagnostic.Create(Descriptor, methodDeclaration.Modifiers[asyncIndex].GetLocation())); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
namespace EcoCode.Extensions; | ||
|
||
/// <summary>Extensions methods for <see cref="SyntaxList{TNode}"/>.</summary> | ||
public static class SyntaxListExtensions | ||
{ | ||
/// <summary>Returns the single node of the list, default if empty or more than one node is contained.</summary> | ||
/// <typeparam name="TNode">The node type.</typeparam> | ||
/// <param name="list">The syntax list.</param> | ||
/// <returns>The single node of the list, default if empty or more than one node is contained.</returns> | ||
public static TNode? SingleOrDefaultNoThrow<TNode>(this SyntaxList<TNode> list) | ||
where TNode : SyntaxNode => list.Count == 1 ? list[0] : default; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.