Skip to content

Commit

Permalink
New rule: MA0147 - Avoid async void method for delegate (#659)
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou committed Dec 5, 2023
1 parent 07c2fea commit 0bb7e9d
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0144](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0144.md)|Performance|Use System.OperatingSystem to check the current OS|⚠️|✔️||
|[MA0145](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0145.md)|Usage|Signature for \[UnsafeAccessorAttribute\] method is not valid|⚠️|✔️||
|[MA0146](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0146.md)|Usage|Name must be set explicitly on local functions|⚠️|✔️||
|[MA0147](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0147.md)|Usage|Avoid async void method for delegate|⚠️|||

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
|[MA0144](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0144.md)|Performance|Use System.OperatingSystem to check the current OS|<span title='Warning'>⚠️</span>|✔️||
|[MA0145](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0145.md)|Usage|Signature for \[UnsafeAccessorAttribute\] method is not valid|<span title='Warning'>⚠️</span>|✔️||
|[MA0146](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0146.md)|Usage|Name must be set explicitly on local functions|<span title='Warning'>⚠️</span>|✔️||
|[MA0147](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0147.md)|Usage|Avoid async void method for delegate|<span title='Warning'>⚠️</span>|||

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -590,6 +591,9 @@ dotnet_diagnostic.MA0145.severity = warning
# MA0146: Name must be set explicitly on local functions
dotnet_diagnostic.MA0146.severity = warning
# MA0147: Avoid async void method for delegate
dotnet_diagnostic.MA0147.severity = none
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1029,4 +1033,7 @@ dotnet_diagnostic.MA0145.severity = none
# MA0146: Name must be set explicitly on local functions
dotnet_diagnostic.MA0146.severity = none
# MA0147: Avoid async void method for delegate
dotnet_diagnostic.MA0147.severity = none
```
9 changes: 9 additions & 0 deletions docs/Rules/MA0147.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# MA0147 - Avoid async void method for delegate

````c#
Foo(() => {}); // ok
Foo(async () => {}); // non-compliant as the delegate is not expecting an async method
void Foo(System.Action action) => throw null;
````
24 changes: 20 additions & 4 deletions src/Meziantou.Analyzer/Internals/AwaitableTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ internal sealed class AwaitableTypes
public AwaitableTypes(Compilation compilation)
{
INotifyCompletionSymbol = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.INotifyCompletion");
AsyncMethodBuilderSymbol = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.AsyncMethodBuilderAttribute");
AsyncMethodBuilderAttributeSymbol = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.AsyncMethodBuilderAttribute");
IAsyncEnumerableSymbol = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1");
IAsyncEnumeratorSymbol = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerator`1");
TaskSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task");
TaskOfTSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1");

if (INotifyCompletionSymbol != null)
{
var taskLikeSymbols = new List<INamedTypeSymbol>(4);
taskLikeSymbols.AddIfNotNull(TaskSymbol);
taskLikeSymbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"));
taskLikeSymbols.AddIfNotNull(TaskOfTSymbol);
taskLikeSymbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask"));
taskLikeSymbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1"));
_taskOrValueTaskSymbols = taskLikeSymbols.ToArray();
Expand All @@ -32,8 +33,9 @@ public AwaitableTypes(Compilation compilation)
}

private INamedTypeSymbol? TaskSymbol { get; }
private INamedTypeSymbol? TaskOfTSymbol { get; }
private INamedTypeSymbol? INotifyCompletionSymbol { get; }
private INamedTypeSymbol? AsyncMethodBuilderSymbol { get; }
private INamedTypeSymbol? AsyncMethodBuilderAttributeSymbol { get; }
public INamedTypeSymbol? IAsyncEnumerableSymbol { get; }
public INamedTypeSymbol? IAsyncEnumeratorSymbol { get; }

Expand Down Expand Up @@ -139,7 +141,21 @@ public bool DoesNotReturnVoidAndCanUseAsyncKeyword(IMethodSymbol method, Semanti
if (method.ReturnType is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.ConstructedFrom.IsEqualToAny(IAsyncEnumerableSymbol, IAsyncEnumeratorSymbol))
return true;

if (AsyncMethodBuilderSymbol != null && method.ReturnType.HasAttribute(AsyncMethodBuilderSymbol))
if (AsyncMethodBuilderAttributeSymbol != null && method.ReturnType.HasAttribute(AsyncMethodBuilderAttributeSymbol))
return true;

return false;
}

public bool IsAsyncBuildableAndNotVoid(ITypeSymbol? symbol)
{
if (symbol is null)
return false;

if (symbol.OriginalDefinition.IsEqualToAny(TaskSymbol, TaskOfTSymbol))
return true;

if (symbol.HasAttribute(AsyncMethodBuilderAttributeSymbol))
return true;

return false;
Expand Down
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ internal static class RuleIdentifiers
public const string UseOperatingSystemInsteadOfRuntimeInformation = "MA0144";
public const string UnsafeAccessorAttribute_InvalidSignature = "MA0145";
public const string UnsafeAccessorAttribute_NameMustBeSet = "MA0146";
public const string DoNotUseAsyncDelegateForSyncDelegate = "MA0147";

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

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DoNotUseAsyncDelegateForSyncDelegateAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor s_rule = new(
RuleIdentifiers.DoNotUseAsyncDelegateForSyncDelegate,
title: "Avoid async void method for delegate",
messageFormat: "Avoid async void method for delegate",
RuleCategories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotUseAsyncDelegateForSyncDelegate));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterOperationAction(AnalyzerDelegateCreationOperation, OperationKind.DelegateCreation);
}

private static void AnalyzerDelegateCreationOperation(OperationAnalysisContext context)
{
var operation = (IDelegateCreationOperation)context.Operation;
if (operation.Type is INamedTypeSymbol { DelegateInvokeMethod: IMethodSymbol delegateInvokeMethod })
{
if (!delegateInvokeMethod.ReturnsVoid)
return;
}
else
{
// Cannot determine the delegate type
return;
}

if (operation.Target is IAnonymousFunctionOperation { Symbol: { IsAsync: true } })
{
context.ReportDiagnostic(s_rule, operation);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,6 @@ private bool IsAsyncContext(IOperation operation, CancellationToken cancellation
{
if (_awaitableTypes.DoesNotReturnVoidAndCanUseAsyncKeyword(methodSymbol, operation.SemanticModel, cancellationToken))
return true;

}

return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Threading.Tasks;
using Meziantou.Analyzer.Rules;
using TestHelper;
using Xunit;

namespace Meziantou.Analyzer.Test.Rules;

public sealed class DoNotUseAsyncDelegateForSyncDelegateAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<DoNotUseAsyncDelegateForSyncDelegateAnalyzer>()
.WithTargetFramework(TargetFramework.Net8_0)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication);
}

[Fact]
public async Task List_ForEach_Sync()
{
await CreateProjectBuilder()
.WithSourceCode("""
new System.Collections.Generic.List<int>().ForEach(item => {});
""")
.ValidateAsync();
}

[Fact]
public async Task List_ForEach_Async()
{
await CreateProjectBuilder()
.WithSourceCode("""
new System.Collections.Generic.List<int>().ForEach([|async item => {}|]);
""")
.ValidateAsync();
}

[Fact]
public async Task CustomDelegate_Sync()
{
await CreateProjectBuilder()
.WithSourceCode("""
A(() => {});

void A(D a) => throw null;
delegate void D();
""")
.ValidateAsync();
}

[Fact]
public async Task CustomDelegate_AsyncVoid()
{
await CreateProjectBuilder()
.WithSourceCode("""
A([|async () => {}|]);

void A(D a) => throw null;
delegate void D();
""")
.ValidateAsync();
}

[Fact]
public async Task Action_Sync()
{
await CreateProjectBuilder()
.WithSourceCode("""
A(() => {});

void A(System.Action a) => throw null;
""")
.ValidateAsync();
}

[Fact]
public async Task Action_AsyncVoid()
{
await CreateProjectBuilder()
.WithSourceCode("""
A([|async () => {}|]);

void A(System.Action a) => throw null;
""")
.ValidateAsync();
}

[Fact]
public async Task FuncTask_AsyncDelegate()
{
await CreateProjectBuilder()
.WithSourceCode("""
A(async () => {});

void A(System.Func<System.Threading.Tasks.Task> a) => throw null;
""")
.ValidateAsync();
}

[Fact]
public async Task FuncValueTask_AsyncDelegate()
{
await CreateProjectBuilder()
.WithSourceCode("""
A(async () => {});

void A(System.Func<System.Threading.Tasks.ValueTask> a) => throw null;
""")
.ValidateAsync();
}

[Fact]
public async Task FuncValueTaskOfInt_AsyncDelegate()
{
await CreateProjectBuilder()
.WithSourceCode("""
A(async () => 1);

void A(System.Func<System.Threading.Tasks.ValueTask<int>> a) => throw null;
""")
.ValidateAsync();
}
}

0 comments on commit 0bb7e9d

Please sign in to comment.