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

Commit 1e10ec1

Browse files
committed
Add 'AvoidAsyncSuffix' analyzer and code fix
Fixes #8
1 parent 72c6873 commit 1e10ec1

File tree

7 files changed

+331
-0
lines changed

7 files changed

+331
-0
lines changed

AsyncUsageAnalyzers/AsyncUsageAnalyzers.Test/AsyncUsageAnalyzers.Test.csproj

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

AsyncUsageAnalyzers/AsyncUsageAnalyzers/AsyncUsageAnalyzers.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
<Compile Include="AnalyzerExtensions.cs" />
4747
<Compile Include="GeneratedCodeAnalysisExtensions.cs" />
4848
<Compile Include="Helpers\RenameHelper.cs" />
49+
<Compile Include="Naming\AvoidAsyncSuffixAnalyzer.cs" />
50+
<Compile Include="Naming\AvoidAsyncSuffixCodeFixProvider.cs" />
4951
<Compile Include="Naming\NamingResources.Designer.cs">
5052
<AutoGen>True</AutoGen>
5153
<DesignTime>True</DesignTime>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 methods which are not asynchronous according to the Task-based Asynchronous Pattern
11+
/// (TAP) by their signature, and reports a warning if the method name includes the suffix <c>Async</c>.
12+
/// </summary>
13+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
14+
public class AvoidAsyncSuffixAnalyzer : DiagnosticAnalyzer
15+
{
16+
/// <summary>
17+
/// The ID for diagnostics produced by the <see cref="AvoidAsyncSuffixAnalyzer"/> analyzer.
18+
/// </summary>
19+
public const string DiagnosticId = "AvoidAsyncSuffix";
20+
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(NamingResources.AvoidAsyncSuffixTitle), NamingResources.ResourceManager, typeof(NamingResources));
21+
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(NamingResources.AvoidAsyncSuffixMessageFormat), NamingResources.ResourceManager, typeof(NamingResources));
22+
private static readonly string Category = "AsyncUsage.CSharp.Naming";
23+
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(NamingResources.AvoidAsyncSuffixDescription), 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.IsAsync)
51+
return;
52+
53+
if (!symbol.Name.EndsWith("Async", StringComparison.Ordinal))
54+
return;
55+
56+
if (symbol.Locations.IsDefaultOrEmpty)
57+
return;
58+
59+
Location location = symbol.Locations[0];
60+
if (!location.IsInSource || location.SourceTree.IsGeneratedDocument(context.CancellationToken))
61+
return;
62+
63+
if (!symbol.ReturnsVoid)
64+
{
65+
if (string.Equals(nameof(Task), symbol.ReturnType?.Name, StringComparison.Ordinal)
66+
&& string.Equals(typeof(Task).Namespace, symbol.ReturnType?.ContainingNamespace?.ToString(), StringComparison.Ordinal))
67+
{
68+
return;
69+
}
70+
}
71+
72+
73+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, symbol.Locations[0], symbol.Name));
74+
}
75+
}
76+
}
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="AvoidAsyncSuffixAnalyzer"/>.
13+
/// </summary>
14+
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = nameof(AvoidAsyncSuffixCodeFixProvider))]
15+
[Shared]
16+
public class AvoidAsyncSuffixCodeFixProvider : CodeFixProvider
17+
{
18+
private static readonly ImmutableArray<string> FixableDiagnostics =
19+
ImmutableArray.Create(AvoidAsyncSuffixAnalyzer.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(AvoidAsyncSuffixAnalyzer.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.Substring(0, token.ValueText.Length - "Async".Length);
50+
context.RegisterCodeFix(CodeAction.Create($"Rename method to '{newName}'", cancellationToken => RenameHelper.RenameSymbolAsync(document, root, token, newName, cancellationToken)), diagnostic);
51+
}
52+
}
53+
}
54+
}

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
@@ -117,4 +117,13 @@
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="AvoidAsyncSuffixDescription" xml:space="preserve">
121+
<value>Only methods which return a Task should include the suffix 'Async'</value>
122+
</data>
123+
<data name="AvoidAsyncSuffixMessageFormat" xml:space="preserve">
124+
<value>Non-Task-returning method '{0}' should not end with 'Async'</value>
125+
</data>
126+
<data name="AvoidAsyncSuffixTitle" xml:space="preserve">
127+
<value>Avoid Async suffix</value>
128+
</data>
120129
</root>

0 commit comments

Comments
 (0)