Skip to content

Commit

Permalink
MA0152: Use Unwrap instead of using await twice
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou committed Dec 29, 2023
1 parent 7944ccd commit 2636f8d
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 9 deletions.
7 changes: 4 additions & 3 deletions README.md
Expand Up @@ -158,15 +158,16 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0140](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0140.md)|Design|Both if and else branch have identical code|⚠️|✔️||
|[MA0141](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0141.md)|Usage|Use pattern matching instead of inequality operators for null check|ℹ️||✔️|
|[MA0142](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0142.md)|Usage|Use pattern matching instead of equality operators for null check|ℹ️||✔️|
|[MA0143](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0143.md)|Design|Primary constructor parameters should be readonly|⚠️|||
|[MA0143](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0143.md)|Design|Primary constructor parameters should be readonly|⚠️|✔️||
|[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|⚠️|||
|[MA0147](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0147.md)|Usage|Avoid async void method for delegate|⚠️|✔️||
|[MA0148](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0148.md)|Usage|Use pattern matching instead of equality operators for discrete value|ℹ️||✔️|
|[MA0149](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0149.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️||✔️|
|[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|⚠️|||
|[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|⚠️|✔️||
|[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|⚠️|✔️||
|[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|ℹ️|✔️||

<!-- rules -->

Expand Down
19 changes: 13 additions & 6 deletions docs/README.md
Expand Up @@ -142,15 +142,16 @@
|[MA0140](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0140.md)|Design|Both if and else branch have identical code|<span title='Warning'>⚠️</span>|✔️||
|[MA0141](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0141.md)|Usage|Use pattern matching instead of inequality operators for null check|<span title='Info'>ℹ️</span>||✔️|
|[MA0142](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0142.md)|Usage|Use pattern matching instead of equality operators for null check|<span title='Info'>ℹ️</span>||✔️|
|[MA0143](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0143.md)|Design|Primary constructor parameters should be readonly|<span title='Warning'>⚠️</span>|||
|[MA0143](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0143.md)|Design|Primary constructor parameters should be readonly|<span title='Warning'>⚠️</span>|✔️||
|[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>|||
|[MA0147](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0147.md)|Usage|Avoid async void method for delegate|<span title='Warning'>⚠️</span>|✔️||
|[MA0148](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0148.md)|Usage|Use pattern matching instead of equality operators for discrete value|<span title='Info'>ℹ️</span>||✔️|
|[MA0149](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0149.md)|Usage|Use pattern matching instead of inequality operators for discrete value|<span title='Info'>ℹ️</span>||✔️|
|[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|<span title='Warning'>⚠️</span>|||
|[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|<span title='Warning'>⚠️</span>|✔️||
|[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|<span title='Warning'>⚠️</span>|✔️||
|[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|<span title='Info'>ℹ️</span>|✔️||

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -585,7 +586,7 @@ dotnet_diagnostic.MA0141.severity = none
dotnet_diagnostic.MA0142.severity = none
# MA0143: Primary constructor parameters should be readonly
dotnet_diagnostic.MA0143.severity = none
dotnet_diagnostic.MA0143.severity = warning
# MA0144: Use System.OperatingSystem to check the current OS
dotnet_diagnostic.MA0144.severity = warning
Expand All @@ -597,7 +598,7 @@ dotnet_diagnostic.MA0145.severity = warning
dotnet_diagnostic.MA0146.severity = warning
# MA0147: Avoid async void method for delegate
dotnet_diagnostic.MA0147.severity = none
dotnet_diagnostic.MA0147.severity = warning
# MA0148: Use pattern matching instead of equality operators for discrete value
dotnet_diagnostic.MA0148.severity = none
Expand All @@ -606,10 +607,13 @@ dotnet_diagnostic.MA0148.severity = none
dotnet_diagnostic.MA0149.severity = none
# MA0150: Do not call the default object.ToString explicitly
dotnet_diagnostic.MA0150.severity = none
dotnet_diagnostic.MA0150.severity = warning
# MA0151: DebuggerDisplay must contain valid members
dotnet_diagnostic.MA0151.severity = warning
# MA0152: Use Unwrap instead of using await twice
dotnet_diagnostic.MA0152.severity = suggestion
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1064,4 +1068,7 @@ dotnet_diagnostic.MA0150.severity = none
# MA0151: DebuggerDisplay must contain valid members
dotnet_diagnostic.MA0151.severity = none
# MA0152: Use Unwrap instead of using await twice
dotnet_diagnostic.MA0152.severity = none
```
10 changes: 10 additions & 0 deletions docs/Rules/MA0152.md
@@ -0,0 +1,10 @@
# MA0152 - Use Unwrap instead of using await twice

Prefer using [`Unwrap`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskextensions.unwrap?view=net-8.0&WT.mc_id=DT-MVP-5003978) instead of using `await` twice

````c#
Task<Task> t;
await await t; // non-compliant
await t.Unwrap(); // compliant
````
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Expand Up @@ -154,6 +154,7 @@ internal static class RuleIdentifiers
public const string UsePatternMatchingForInequalityComparison = "MA0149";
public const string DoNotUseToStringIfObject = "MA0150";
public const string DebuggerDisplayAttributeShouldContainValidExpressions = "MA0151";
public const string UseTaskUnwrap = "MA0152";

public static string GetHelpUri(string identifier)
{
Expand Down
100 changes: 100 additions & 0 deletions src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs
@@ -0,0 +1,100 @@
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 UseTaskUnwrapAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.UseTaskUnwrap,
title: "Use Unwrap instead of double await",
messageFormat: "Use Unwrap instead of double await",
RuleCategories.Performance,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseTaskUnwrap));

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

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

context.RegisterCompilationStartAction(context =>
{
var ctx = new AnalyzerContext(context.Compilation);
if (!ctx.IsValid)
return;
context.RegisterOperationAction(ctx.AnalyzeAwait, OperationKind.Await);
});
}

private sealed class AnalyzerContext
{
public AnalyzerContext(Compilation compilation)
{
TaskSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task");
TaskOfTSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1");

ConfiguredTaskAwaitableSymbol = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.ConfiguredTaskAwaitable");
ConfiguredTaskAwaitableOfTSymbol = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1");

if (TaskSymbol is not null && TaskOfTSymbol is not null)
{
TaskOfTaskSymbol = TaskOfTSymbol.Construct(TaskSymbol);
TaskOfTaskOfTSymbol = TaskOfTSymbol.Construct(TaskOfTSymbol);
}
}

public INamedTypeSymbol? TaskSymbol { get; }
public INamedTypeSymbol? TaskOfTSymbol { get; }
public INamedTypeSymbol? TaskOfTaskSymbol { get; }
public INamedTypeSymbol? TaskOfTaskOfTSymbol { get; }

public INamedTypeSymbol? ConfiguredTaskAwaitableSymbol { get; }
public INamedTypeSymbol? ConfiguredTaskAwaitableOfTSymbol { get; }

public bool IsValid => TaskOfTaskSymbol is not null || TaskOfTaskOfTSymbol is not null;

public void AnalyzeAwait(OperationAnalysisContext context)
{
var operation = (IAwaitOperation)context.Operation;

if (operation.Operation is IAwaitOperation childAwaitOperation)
{
if (childAwaitOperation.Operation.Type is not INamedTypeSymbol childAwaitOperationType)
return;

// Task<Task>
if (childAwaitOperationType.IsEqualTo(TaskOfTaskSymbol))
{
context.ReportDiagnostic(Rule, operation);
}
// Task<Task<T>>
else if (childAwaitOperationType.OriginalDefinition.IsEqualTo(TaskOfTSymbol) && childAwaitOperationType.TypeArguments[0].OriginalDefinition.IsEqualTo(TaskOfTSymbol))
{
context.ReportDiagnostic(Rule, operation);
}
}
else if (operation.Operation is IInvocationOperation { Instance: IAwaitOperation { Operation.Type: INamedTypeSymbol childAwaitOperationType }, Type: var invocationType } && invocationType.IsEqualToAny(ConfiguredTaskAwaitableSymbol, ConfiguredTaskAwaitableOfTSymbol))
{
// Task<Task>
if (childAwaitOperationType.IsEqualTo(TaskOfTaskSymbol))
{
context.ReportDiagnostic(Rule, operation);
}
// Task<Task<T>>
else if (childAwaitOperationType.OriginalDefinition.IsEqualTo(TaskOfTSymbol) && childAwaitOperationType.TypeArguments[0].OriginalDefinition.IsEqualTo(TaskOfTSymbol))
{
context.ReportDiagnostic(Rule, operation);
}
}
}
}
}
109 changes: 109 additions & 0 deletions tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs
@@ -0,0 +1,109 @@
using System.Threading.Tasks;
using Meziantou.Analyzer.Rules;
using TestHelper;
using Xunit;

namespace Meziantou.Analyzer.Test.Rules;

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

[Fact]
public async Task TaskOfTask()
{
await CreateProjectBuilder()
.WithSourceCode("""
using System.Threading.Tasks;

Task<Task> a = null;
[|await await a|];
""")
.ValidateAsync();
}

[Fact]
public async Task TaskOfTask_ConfigureAwait()
{
await CreateProjectBuilder()
.WithSourceCode("""
using System.Threading.Tasks;

Task<Task> a = null;
await (await a.ConfigureAwait(false));
""")
.ValidateAsync();
}

[Fact]
public async Task TaskOfTask_ConfigureAwait_Root()
{
await CreateProjectBuilder()
.WithSourceCode("""
using System.Threading.Tasks;

Task<Task> a = null;
[|await (await a).ConfigureAwait(false)|];
""")
.ValidateAsync();
}

[Fact]
public async Task TaskOfTask_Unwrap_ConfigureAwait_Root()
{
await CreateProjectBuilder()
.WithSourceCode("""
using System.Threading.Tasks;

Task<Task> a = null;
await a.Unwrap().ConfigureAwait(false);
""")
.ValidateAsync();
}

[Fact]
public async Task TaskOfTaskOfInt32()
{
await CreateProjectBuilder()
.WithSourceCode("""
using System.Threading.Tasks;

Task<Task<int>> a = null;
int b = [|await await a|];
""")
.ValidateAsync();
}

[Fact]
public async Task TaskOfValueTaskOfInt32()
{
await CreateProjectBuilder()
.WithSourceCode("""
using System.Threading.Tasks;

Task<ValueTask<int>> a = null;
int b = await await a;
""")
.ValidateAsync();
}

[Fact]
public async Task ValueTaskOfTaskOfInt32()
{
await CreateProjectBuilder()
.WithSourceCode("""
using System.Threading.Tasks;

ValueTask<Task<int>> a = default;
int b = await await a;
""")
.ValidateAsync();
}

}

0 comments on commit 2636f8d

Please sign in to comment.