Skip to content

Commit 2cb4eb7

Browse files
authored
Async Module Analyzer (#359)
* Async Module Analyzer * Formatting Markdown * Nearly there * Fix code fix * ReleaseNotes.md * Nearly * WIP * Fix * Merge main * Formatting Markdown --------- Co-authored-by: Tom Longhurst <thomhurst@users.noreply.github.com>
1 parent 9b17f95 commit 2cb4eb7

File tree

14 files changed

+569
-15
lines changed

14 files changed

+569
-15
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ Define your pipeline in .NET! Strong types, intellisense, parallelisation, and t
6363
| ModularPipelines.WinGet | Helpers for interacting with the Windows Package Manager. | [![nuget](https://img.shields.io/nuget/v/ModularPipelines.WinGet.svg)](https://www.nuget.org/packages/ModularPipelines.WinGet/) |
6464
| ModularPipelines.Yarn | Helpers for interacting with Yarn CLI. | [![nuget](https://img.shields.io/nuget/v/ModularPipelines.Yarn.svg)](https://www.nuget.org/packages/ModularPipelines.Yarn/) |
6565

66-
6766
## Getting Started
6867

6968
If you want to see how to get started, or want to know more about ModularPipelines, [read the Documentation here](https://thomhurst.github.io/ModularPipelines)
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Diagnostics.CodeAnalysis;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CodeActions;
6+
using Microsoft.CodeAnalysis.CodeFixes;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Editing;
10+
11+
namespace ModularPipelines.Analyzers;
12+
13+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AsyncModuleCodeFixProvider))]
14+
[Shared]
15+
[ExcludeFromCodeCoverage]
16+
public class AsyncModuleCodeFixProvider : CodeFixProvider
17+
{
18+
/// <inheritdoc/>
19+
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AsyncModuleAnalyzer.DiagnosticId);
20+
21+
/// <inheritdoc/>
22+
public sealed override FixAllProvider GetFixAllProvider()
23+
{
24+
// See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
25+
return WellKnownFixAllProviders.BatchFixer;
26+
}
27+
28+
/// <inheritdoc/>
29+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
30+
{
31+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
32+
33+
if (root is null)
34+
{
35+
return;
36+
}
37+
38+
var diagnostic = context.Diagnostics.First();
39+
var diagnosticSpan = diagnostic.Location.SourceSpan;
40+
41+
// Find the type declaration identified by the diagnostic.
42+
var declaration = root.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<MethodDeclarationSyntax>().First();
43+
44+
if (declaration is null)
45+
{
46+
return;
47+
}
48+
49+
// Register a code action that will invoke the fix.
50+
context.RegisterCodeFix(
51+
CodeAction.Create(
52+
title: CodeFixResources.AsyncModuleCodeFixTitle,
53+
createChangedDocument: c => AddAsync(context, declaration, c),
54+
equivalenceKey: nameof(CodeFixResources.AsyncModuleCodeFixTitle)),
55+
diagnostic);
56+
}
57+
58+
private async Task<Document> AddAsync(CodeFixContext context, MethodDeclarationSyntax methodDeclarationSyntax, CancellationToken cancellationToken)
59+
{
60+
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken);
61+
62+
editor.SetModifiers(methodDeclarationSyntax, DeclarationModifiers.Override | DeclarationModifiers.Async);
63+
64+
foreach (var returnStatement in GetReturnStatements(methodDeclarationSyntax))
65+
{
66+
var expressionSyntax = returnStatement.ChildNodes().OfType<ExpressionSyntax>().First()!;
67+
68+
if (await IsTaskFromResult(expressionSyntax, context)
69+
|| await IsAsTaskExtension(expressionSyntax, context))
70+
{
71+
var firstInnerExpression = expressionSyntax.ChildNodes().OfType<ArgumentListSyntax>().First().Arguments.First().Expression;
72+
73+
var newReturnStatement = returnStatement.ReplaceNode(expressionSyntax, firstInnerExpression);
74+
75+
editor.ReplaceNode(returnStatement, newReturnStatement);
76+
77+
continue;
78+
}
79+
80+
var awaitExpressionSyntax = SyntaxFactory.AwaitExpression(expressionSyntax).NormalizeWhitespace();
81+
82+
var awaitedReturnStatement = returnStatement.ReplaceNode(expressionSyntax, awaitExpressionSyntax);
83+
84+
editor.ReplaceNode(returnStatement, awaitedReturnStatement);
85+
}
86+
87+
return editor.GetChangedDocument();
88+
}
89+
90+
private static ReturnStatementSyntax[] GetReturnStatements(MethodDeclarationSyntax newMethodDeclarationSyntax)
91+
{
92+
return newMethodDeclarationSyntax.Body
93+
?.DescendantNodes()
94+
.OfType<ReturnStatementSyntax>()
95+
.Reverse()
96+
.ToArray()
97+
?? Array.Empty<ReturnStatementSyntax>();
98+
}
99+
100+
private async Task<bool> IsTaskFromResult(ExpressionSyntax expressionSyntax, CodeFixContext context)
101+
{
102+
if (expressionSyntax is not InvocationExpressionSyntax invocationExpressionSyntax)
103+
{
104+
return false;
105+
}
106+
107+
var semanticModel = await context.Document.GetSemanticModelAsync();
108+
109+
var symbol = semanticModel.GetSymbolInfo(invocationExpressionSyntax).Symbol;
110+
111+
if (symbol is not IMethodSymbol methodSymbol)
112+
{
113+
return false;
114+
}
115+
116+
return
117+
methodSymbol.Name == "FromResult"
118+
&& methodSymbol.ContainingType.Name == "Task"
119+
&& methodSymbol.ContainingNamespace.ToDisplayString() == "System.Threading.Tasks";
120+
}
121+
122+
private async Task<bool> IsAsTaskExtension(ExpressionSyntax expressionSyntax, CodeFixContext context)
123+
{
124+
if (expressionSyntax is not InvocationExpressionSyntax invocationExpressionSyntax)
125+
{
126+
return false;
127+
}
128+
129+
var semanticModel = await context.Document.GetSemanticModelAsync();
130+
131+
var symbol = semanticModel.GetSymbolInfo(invocationExpressionSyntax).Symbol;
132+
133+
if (symbol is not IMethodSymbol methodSymbol)
134+
{
135+
return false;
136+
}
137+
138+
return
139+
methodSymbol.Name == "AsTask"
140+
&& methodSymbol.ContainingType.Name == "TaskExtensions"
141+
&& methodSymbol.ContainingNamespace.ToDisplayString() == "ModularPipelines.Extensions";
142+
}
143+
}

src/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.Designer.cs

Lines changed: 13 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,11 @@
117117
<resheader name="writer">
118118
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119119
</resheader>
120-
<data name="CodeFixTitle" xml:space="preserve">
120+
<data name="MissingDependsOnAttributeCodeFixTitle" xml:space="preserve">
121121
<value>Add DependsOn Attribute</value>
122122
<comment>The title of the code fix.</comment>
123123
</data>
124+
<data name="AsyncModuleCodeFixTitle" xml:space="preserve">
125+
<value>Add Async Modifier</value>
126+
</data>
124127
</root>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace ModularPipelines.Analyzers.Extensions;
4+
5+
internal static class DocumentExtensions
6+
{
7+
public static async Task<Document> WithReplacedNode(
8+
this Document document,
9+
SyntaxNode oldNode,
10+
SyntaxNode newNode,
11+
SyntaxNode? root = null)
12+
{
13+
root ??= await document
14+
.GetSyntaxRootAsync()
15+
.ConfigureAwait(false);
16+
17+
var newRoot = root!.ReplaceNode(oldNode, newNode);
18+
19+
return document.WithSyntaxRoot(newRoot);
20+
}
21+
}

src/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/MissingDependsOnAttributeCodeFixProvider.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
4949
// Register a code action that will invoke the fix.
5050
context.RegisterCodeFix(
5151
CodeAction.Create(
52-
title: CodeFixResources.CodeFixTitle,
52+
title: CodeFixResources.MissingDependsOnAttributeCodeFixTitle,
5353
createChangedDocument: c => AddAttribute(context, declaration, c),
54-
equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
54+
equivalenceKey: nameof(CodeFixResources.MissingDependsOnAttributeCodeFixTitle)),
5555
diagnostic);
5656
}
5757

0 commit comments

Comments
 (0)