Skip to content
This repository was archived by the owner on Nov 8, 2018. It is now read-only.

Commit 9ebdc66

Browse files
committed
Add 'UseAsyncSuffix' analyzer and code fix
Fixes #7
1 parent 1e10ec1 commit 9ebdc66

File tree

7 files changed

+322
-0
lines changed

7 files changed

+322
-0
lines changed

AsyncUsageAnalyzers/AsyncUsageAnalyzers.Test/AsyncUsageAnalyzers.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
</ItemGroup>
124124
<ItemGroup>
125125
<Compile Include="Naming\AvoidAsyncSuffixUnitTests.cs" />
126+
<Compile Include="Naming\UseAsyncSuffixUnitTests.cs" />
126127
<Compile Include="Verifiers\CodeFixVerifier.cs" />
127128
<Compile Include="Verifiers\DiagnosticVerifier.cs" />
128129
<Compile Include="Helpers\CodeFixVerifier.Helper.cs" />
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
namespace AsyncUsageAnalyzers.Test.Naming
2+
{
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using AsyncUsageAnalyzers.Naming;
6+
using Microsoft.CodeAnalysis.CodeFixes;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using TestHelper;
9+
using Xunit;
10+
11+
public class UseAsyncSuffixUnitTests : CodeFixVerifier
12+
{
13+
[Fact]
14+
public async Task TestAsyncReturnVoidAsync()
15+
{
16+
string testCode = @"
17+
class ClassName
18+
{
19+
async void FirstMethod() { }
20+
async void SecondMethodAsync() { }
21+
}
22+
";
23+
24+
await VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
25+
}
26+
27+
[Fact]
28+
public async Task TestAsyncEventHandlerReturnVoidAsync()
29+
{
30+
string testCode = @"
31+
using System;
32+
class ClassName
33+
{
34+
async void FirstMethod(object sender, EventArgs e) { }
35+
async void SecondMethodAsync(object sender, EventArgs e) { }
36+
}
37+
";
38+
39+
await VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
40+
}
41+
42+
[Fact]
43+
public async Task TestAsyncReturnTaskAsync()
44+
{
45+
string testCode = @"
46+
using System.Threading.Tasks;
47+
class ClassName
48+
{
49+
async Task FirstMethod() { }
50+
async Task SecondMethodAsync() { }
51+
}
52+
";
53+
string fixedCode = @"
54+
using System.Threading.Tasks;
55+
class ClassName
56+
{
57+
async Task FirstMethodAsync() { }
58+
async Task SecondMethodAsync() { }
59+
}
60+
";
61+
62+
DiagnosticResult expected = CSharpDiagnostic().WithArguments("FirstMethod").WithLocation(5, 16);
63+
await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
64+
await VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
65+
await VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
66+
}
67+
68+
[Fact]
69+
public async Task TestAsyncReturnGenericTaskAsync()
70+
{
71+
string testCode = @"
72+
using System.Threading.Tasks;
73+
class ClassName
74+
{
75+
async Task<int> FirstMethod() { return 3; }
76+
async Task<int> SecondMethodAsync() { return 3; }
77+
}
78+
";
79+
string fixedCode = @"
80+
using System.Threading.Tasks;
81+
class ClassName
82+
{
83+
async Task<int> FirstMethodAsync() { return 3; }
84+
async Task<int> SecondMethodAsync() { return 3; }
85+
}
86+
";
87+
88+
DiagnosticResult expected = CSharpDiagnostic().WithArguments("FirstMethod").WithLocation(5, 21);
89+
await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
90+
await VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
91+
await VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
92+
}
93+
94+
[Fact]
95+
public async Task TestReturnTaskAsync()
96+
{
97+
string testCode = @"
98+
using System.Threading.Tasks;
99+
class ClassName
100+
{
101+
Task FirstMethod() { return Task.FromResult(3); }
102+
Task SecondMethodAsync() { return Task.FromResult(3); }
103+
}
104+
";
105+
string fixedCode = @"
106+
using System.Threading.Tasks;
107+
class ClassName
108+
{
109+
Task FirstMethodAsync() { return Task.FromResult(3); }
110+
Task SecondMethodAsync() { return Task.FromResult(3); }
111+
}
112+
";
113+
114+
DiagnosticResult expected = CSharpDiagnostic().WithArguments("FirstMethod").WithLocation(5, 10);
115+
await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
116+
await VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
117+
await VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
118+
}
119+
120+
[Fact]
121+
public async Task TestReturnGenericTaskAsync()
122+
{
123+
string testCode = @"
124+
using System.Threading.Tasks;
125+
class ClassName
126+
{
127+
Task<int> FirstMethod() { return Task.FromResult(3); }
128+
Task<int> SecondMethodAsync() { return Task.FromResult(3); }
129+
}
130+
";
131+
string fixedCode = @"
132+
using System.Threading.Tasks;
133+
class ClassName
134+
{
135+
Task<int> FirstMethodAsync() { return Task.FromResult(3); }
136+
Task<int> SecondMethodAsync() { return Task.FromResult(3); }
137+
}
138+
";
139+
140+
DiagnosticResult expected = CSharpDiagnostic().WithArguments("FirstMethod").WithLocation(5, 15);
141+
await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
142+
await VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
143+
await VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
144+
}
145+
146+
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
147+
{
148+
return new UseAsyncSuffixAnalyzer();
149+
}
150+
151+
protected override CodeFixProvider GetCSharpCodeFixProvider()
152+
{
153+
return new UseAsyncSuffixCodeFixProvider();
154+
}
155+
}
156+
}

AsyncUsageAnalyzers/AsyncUsageAnalyzers/AsyncUsageAnalyzers.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
<DesignTime>True</DesignTime>
5454
<DependentUpon>NamingResources.resx</DependentUpon>
5555
</Compile>
56+
<Compile Include="Naming\UseAsyncSuffixAnalyzer.cs" />
57+
<Compile Include="Naming\UseAsyncSuffixCodeFixProvider.cs" />
5658
<Compile Include="NoCodeFixAttribute.cs" />
5759
<Compile Include="Properties\AssemblyInfo.cs" />
5860
<Compile Include="Resources.Designer.cs">

AsyncUsageAnalyzers/AsyncUsageAnalyzers/Naming/NamingResources.Designer.cs

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

AsyncUsageAnalyzers/AsyncUsageAnalyzers/Naming/NamingResources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,13 @@
126126
<data name="AvoidAsyncSuffixTitle" xml:space="preserve">
127127
<value>Avoid Async suffix</value>
128128
</data>
129+
<data name="UseAsyncSuffixDescription" xml:space="preserve">
130+
<value>Methods which return a Task should include the suffix 'Async'</value>
131+
</data>
132+
<data name="UseAsyncSuffixMessageFormat" xml:space="preserve">
133+
<value>Task-returning method '{0}' should end with 'Async'</value>
134+
</data>
135+
<data name="UseAsyncSuffixTitle" xml:space="preserve">
136+
<value>Use Async suffix</value>
137+
</data>
129138
</root>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
namespace AsyncUsageAnalyzers.Naming
2+
{
3+
using System;
4+
using System.Collections.Immutable;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
9+
/// <summary>
10+
/// This analyzer identifies asynchronous methods using the Task-based Asynchronous Pattern (TAP) according to their
11+
/// signature, and reports a warning if the method name does not include the suffix <c>Async</c>.
12+
/// </summary>
13+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
14+
public class UseAsyncSuffixAnalyzer : DiagnosticAnalyzer
15+
{
16+
/// <summary>
17+
/// The ID for diagnostics produced by the <see cref="UseAsyncSuffixAnalyzer"/> analyzer.
18+
/// </summary>
19+
public const string DiagnosticId = "UseAsyncSuffix";
20+
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(NamingResources.UseAsyncSuffixTitle), NamingResources.ResourceManager, typeof(NamingResources));
21+
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(NamingResources.UseAsyncSuffixMessageFormat), NamingResources.ResourceManager, typeof(NamingResources));
22+
private static readonly string Category = "AsyncUsage.CSharp.Naming";
23+
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(NamingResources.UseAsyncSuffixDescription), NamingResources.ResourceManager, typeof(NamingResources));
24+
private static readonly string HelpLink = null;
25+
26+
private static readonly DiagnosticDescriptor Descriptor =
27+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, true, Description, HelpLink);
28+
29+
private static readonly ImmutableArray<DiagnosticDescriptor> SupportedDiagnosticsValue =
30+
ImmutableArray.Create(Descriptor);
31+
32+
/// <inheritdoc/>
33+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
34+
{
35+
get
36+
{
37+
return SupportedDiagnosticsValue;
38+
}
39+
}
40+
41+
/// <inheritdoc/>
42+
public override void Initialize(AnalysisContext context)
43+
{
44+
context.RegisterSymbolAction(HandleMethodDeclaration, SymbolKind.Method);
45+
}
46+
47+
private void HandleMethodDeclaration(SymbolAnalysisContext context)
48+
{
49+
IMethodSymbol symbol = (IMethodSymbol)context.Symbol;
50+
if (symbol.Name.EndsWith("Async", StringComparison.Ordinal))
51+
return;
52+
53+
if (symbol.Locations.IsDefaultOrEmpty)
54+
return;
55+
56+
Location location = symbol.Locations[0];
57+
if (!location.IsInSource || location.SourceTree.IsGeneratedDocument(context.CancellationToken))
58+
return;
59+
60+
// void-returning methods are not asynchronous according to their signature, even if they use `async`
61+
if (symbol.ReturnsVoid)
62+
return;
63+
64+
if (!string.Equals(nameof(Task), symbol.ReturnType?.Name, StringComparison.Ordinal))
65+
return;
66+
67+
if (!string.Equals(typeof(Task).Namespace, symbol.ReturnType?.ContainingNamespace?.ToString(), StringComparison.Ordinal))
68+
return;
69+
70+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, symbol.Locations[0], symbol.Name));
71+
}
72+
}
73+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
namespace AsyncUsageAnalyzers.Naming
2+
{
3+
using System.Collections.Immutable;
4+
using System.Composition;
5+
using System.Threading.Tasks;
6+
using Helpers;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CodeActions;
9+
using Microsoft.CodeAnalysis.CodeFixes;
10+
11+
/// <summary>
12+
/// Implements a code fix for <see cref="UseAsyncSuffixAnalyzer"/>.
13+
/// </summary>
14+
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = nameof(UseAsyncSuffixCodeFixProvider))]
15+
[Shared]
16+
public class UseAsyncSuffixCodeFixProvider : CodeFixProvider
17+
{
18+
private static readonly ImmutableArray<string> FixableDiagnostics =
19+
ImmutableArray.Create(UseAsyncSuffixAnalyzer.DiagnosticId);
20+
21+
/// <inheritdoc/>
22+
public override ImmutableArray<string> FixableDiagnosticIds => FixableDiagnostics;
23+
24+
/// <inheritdoc/>
25+
public override FixAllProvider GetFixAllProvider()
26+
{
27+
return WellKnownFixAllProviders.BatchFixer;
28+
}
29+
30+
/// <inheritdoc/>
31+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
32+
{
33+
var document = context.Document;
34+
var root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
35+
36+
foreach (var diagnostic in context.Diagnostics)
37+
{
38+
if (!diagnostic.Id.Equals(UseAsyncSuffixAnalyzer.DiagnosticId))
39+
{
40+
continue;
41+
}
42+
43+
var token = root.FindToken(diagnostic.Location.SourceSpan.Start);
44+
if (token.IsMissing)
45+
{
46+
continue;
47+
}
48+
49+
var newName = token.ValueText + "Async";
50+
context.RegisterCodeFix(CodeAction.Create($"Rename method to '{newName}'", cancellationToken => RenameHelper.RenameSymbolAsync(document, root, token, newName, cancellationToken)), diagnostic);
51+
}
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)