Skip to content

Commit

Permalink
New rules: MA0137 and MA0138 Use 'Async' suffix when a method returns…
Browse files Browse the repository at this point in the history
… an awaitable type
  • Loading branch information
meziantou committed Oct 27, 2023
1 parent 94e3d13 commit c2f6d06
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md)|Usage|Observe result of async calls|⚠️|✔️||
|[MA0135](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0135.md)|Design|The log parameter has no configured type|⚠️|||
|[MA0136](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0136.md)|Usage|Raw String contains an implicit end of line character|👻|✔️||
|[MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md)|Design|Use 'Async' suffix when a method returns an awaitable type|⚠️|||
|[MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md)|Design|Do not use 'Async' suffix when a method does not return an awaitable type|⚠️|||

<!-- rules -->

Expand Down
14 changes: 14 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
|[MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md)|Usage|Observe result of async calls|<span title='Warning'>⚠️</span>|✔️||
|[MA0135](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0135.md)|Design|The log parameter has no configured type|<span title='Warning'>⚠️</span>|||
|[MA0136](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0136.md)|Usage|Raw String contains an implicit end of line character|<span title='Hidden'>👻</span>|✔️||
|[MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md)|Design|Use 'Async' suffix when a method returns an awaitable type|<span title='Warning'>⚠️</span>|||
|[MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md)|Design|Do not use 'Async' suffix when a method does not return an awaitable type|<span title='Warning'>⚠️</span>|||

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -550,6 +552,12 @@ dotnet_diagnostic.MA0135.severity = none
# MA0136: Raw String contains an implicit end of line character
dotnet_diagnostic.MA0136.severity = silent
# MA0137: Use 'Async' suffix when a method returns an awaitable type
dotnet_diagnostic.MA0137.severity = none
# MA0138: Do not use 'Async' suffix when a method does not return an awaitable type
dotnet_diagnostic.MA0138.severity = none
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -959,4 +967,10 @@ dotnet_diagnostic.MA0135.severity = none
# MA0136: Raw String contains an implicit end of line character
dotnet_diagnostic.MA0136.severity = none
# MA0137: Use 'Async' suffix when a method returns an awaitable type
dotnet_diagnostic.MA0137.severity = none
# MA0138: Do not use 'Async' suffix when a method does not return an awaitable type
dotnet_diagnostic.MA0138.severity = none
```
11 changes: 11 additions & 0 deletions docs/Rules/MA0137.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# MA0137 - Use 'Async' suffix when a method returns an awaitable type

Methods that return awaitable types such as `Task` or `ValueTask` should have an Async suffix.

````c#
// compliant
Task FooAsync() => Task.CompletedTask;

// non-compliant
Task Foo() => Task.CompletedTask;
````
11 changes: 11 additions & 0 deletions docs/Rules/MA0138.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# MA0138 - Do not use 'Async' suffix when a method does not return an awaitable type

Methods that does not return an awaitable type such as `Task` or `ValueTask` should not have an 'Async' suffix.

````c#
// compliant
void Foo() { }

// non-compliant
void FooAsync() { }
````
36 changes: 36 additions & 0 deletions src/Meziantou.Analyzer/Internals/AwaitableTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,42 @@ public bool IsAwaitable(ITypeSymbol? symbol, SemanticModel semanticModel, int po
return false;
}

public bool IsAwaitable(ITypeSymbol? symbol, Compilation compilation)
{
if (symbol == null)
return false;

if (INotifyCompletionSymbol == null)
return false;

if (symbol.SpecialType is SpecialType.System_Void || symbol.TypeKind is TypeKind.Dynamic)
return false;

if (IsTaskOrValueTask(symbol))
return true;

var awaiter = symbol.GetMembers("GetAwaiter");

foreach (var potentialSymbol in awaiter)
{
if (potentialSymbol is not IMethodSymbol getAwaiterMethod)
continue;

if (!compilation.IsSymbolAccessibleWithin(potentialSymbol, compilation.Assembly))
continue;

if (!getAwaiterMethod.Parameters.IsEmpty)
continue;

if (!ConformsToAwaiterPattern(getAwaiterMethod.ReturnType))
continue;

return true;
}

return false;
}

public bool DoesNotReturnVoidAndCanUseAsyncKeyword(IMethodSymbol method, SemanticModel semanticModel, CancellationToken cancellationToken)
{
if (method.IsTopLevelStatementsEntryPointMethod())
Expand Down
12 changes: 12 additions & 0 deletions src/Meziantou.Analyzer/Internals/ContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ public static void ReportDiagnostic(this OperationAnalysisContext context, Diagn

context.ReportDiagnostic(descriptor, properties, operation, messageArgs);
}

public static void ReportDiagnostic(this OperationAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, ILocalFunctionOperation operation, DiagnosticReportOptions options, params string?[] messageArgs)
{
if (options.HasFlag(DiagnosticReportOptions.ReportOnMethodName) &&
operation.Syntax is LocalFunctionStatementSyntax memberAccessExpression)
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, memberAccessExpression.Identifier.GetLocation(), properties, messageArgs));
return;
}

context.ReportDiagnostic(descriptor, properties, operation, messageArgs);
}

public static void ReportDiagnostic(this CompilationAnalysisContext context, DiagnosticDescriptor descriptor, ISymbol symbol, params string?[] messageArgs)
{
Expand Down
2 changes: 2 additions & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ internal static class RuleIdentifiers
public const string AwaitAwaitableMethodInSyncMethod = "MA0134";
public const string LoggerParameterType_MissingConfiguration = "MA0135";
public const string RawStringShouldNotContainsNonDeterministicEndOfLine = "MA0136";
public const string MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffix = "MA0137";
public const string MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix = "MA0138";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System;
using System.Collections.Immutable;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor s_asyncSuffixRule = new(
RuleIdentifiers.MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffix,
title: "Use 'Async' suffix when a method returns an awaitable type",
messageFormat: "Method returning an awaitable type must use the 'Async' suffix",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffix));

private static readonly DiagnosticDescriptor s_notAsyncSuffixRule = new(
RuleIdentifiers.MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix,
title: "Do not use 'Async' suffix when a method does not return an awaitable type",
messageFormat: "Method not returning an awaitable type must not use the 'Async' suffix",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_asyncSuffixRule, s_notAsyncSuffixRule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(ctx =>
{
var context = new AnalyzerContext(ctx.Compilation);
ctx.RegisterSymbolAction(context.AnalyzeSymbol, SymbolKind.Method);
ctx.RegisterOperationAction(context.AnalyzeLocalFunction, OperationKind.LocalFunction);
});
}

private sealed class AnalyzerContext
{
private readonly AwaitableTypes _awaitableTypes;

public AnalyzerContext(Compilation compilation)
{
_awaitableTypes = new AwaitableTypes(compilation);
}

public void AnalyzeSymbol(SymbolAnalysisContext context)
{
var method = (IMethodSymbol)context.Symbol;
if (method.IsOverrideOrInterfaceImplementation())
return;

var hasAsyncSuffix = method.Name.EndsWith("Async", StringComparison.Ordinal);
if (_awaitableTypes.IsAwaitable(method.ReturnType, context.Compilation))
{
if (!hasAsyncSuffix)
{
context.ReportDiagnostic(s_asyncSuffixRule, method);
}
}
else
{
if (hasAsyncSuffix)
{
context.ReportDiagnostic(s_notAsyncSuffixRule, method);
}
}
}

public void AnalyzeLocalFunction(OperationAnalysisContext context)
{
var operation = (ILocalFunctionOperation)context.Operation;
var method = operation.Symbol;

var hasAsyncSuffix = method.Name.EndsWith("Async", StringComparison.Ordinal);
if (_awaitableTypes.IsAwaitable(method.ReturnType, context.Compilation))
{
if (!hasAsyncSuffix)
{
context.ReportDiagnostic(s_asyncSuffixRule, properties: default, operation, DiagnosticReportOptions.ReportOnMethodName);
}
}
else
{
if (hasAsyncSuffix)
{
context.ReportDiagnostic(s_notAsyncSuffixRule, properties: default, operation, DiagnosticReportOptions.ReportOnMethodName);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System.Threading.Tasks;
using Meziantou.Analyzer.Rules;
using TestHelper;
using Xunit;

namespace Meziantou.Analyzer.Test.Rules;
public sealed class MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer>()
.WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview);
}

[Fact]
public async Task AsyncMethodWithSuffix()
{
const string SourceCode = """
class TypeName
{
System.Threading.Tasks.Task TestAsync() => throw null;
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task AsyncMethodWithoutSuffix()
{
const string SourceCode = """
class TypeName
{
System.Threading.Tasks.Task [|Test|]() => throw null;
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}
[Fact]
public async Task VoidMethodWithSuffix()
{
const string SourceCode = """
class TypeName
{
void [|TestAsync|]() => throw null;
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task VoidMethodWithoutSuffix()
{
const string SourceCode = """
class TypeName
{
void Test() => throw null;
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task VoidLocalFunctionWithSuffix()
{
const string SourceCode = """
class TypeName
{
void Test()
{
void [|FooAsync|]() => throw null;
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task VoidLocalFunctionWithoutSuffix()
{
const string SourceCode = """
class TypeName
{
void Test()
{
void Foo() => throw null;
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task AwaitableLocalFunctionWithoutSuffix()
{
const string SourceCode = """
class TypeName
{
void Test()
{
_ = Foo();
System.Threading.Tasks.Task [|Foo|]() => throw null;
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task AwaitableLocalFunctionWithSuffix()
{
const string SourceCode = """
class TypeName
{
void Test()
{
System.Threading.Tasks.Task FooAsync() => throw null;
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}
}

0 comments on commit c2f6d06

Please sign in to comment.