Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/Rules/MA0152.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# MA0152 - Use Unwrap instead of using await twice
# MA0152 - Use Unwrap instead of using await twice
<!-- sources -->
Sources: [UseTaskUnwrapAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs), [UseTaskUnwrapFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs)
<!-- sources -->
Expand All @@ -11,3 +11,15 @@ await await t; // non-compliant

await t.Unwrap(); // compliant
````

The rule is not reported when the inner task is awaited with `ConfigureAwaitOptions.SuppressThrowing`, as `Unwrap()` would also suppress the exceptions of the outer task:

````c#
Task<Task> t;

// The exceptions of t are propagated, only the ones of the inner task are suppressed
await (await t).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

// The exceptions of t are suppressed too
await t.Unwrap().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
````
30 changes: 29 additions & 1 deletion src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public AnalyzerContext(Compilation compilation)
ConfiguredTaskAwaitableSymbol = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.ConfiguredTaskAwaitable");
ConfiguredTaskAwaitableOfTSymbol = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1");

ConfigureAwaitOptionsSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ConfigureAwaitOptions");
SuppressThrowingValue = ConfigureAwaitOptionsSymbol?.GetMembers("SuppressThrowing").OfType<IFieldSymbol>().FirstOrDefault()?.ConstantValue as int?;

if (TaskSymbol is not null && TaskOfTSymbol is not null)
{
TaskOfTaskSymbol = TaskOfTSymbol.Construct(TaskSymbol);
Expand All @@ -55,6 +58,9 @@ public AnalyzerContext(Compilation compilation)
public INamedTypeSymbol? ConfiguredTaskAwaitableSymbol { get; }
public INamedTypeSymbol? ConfiguredTaskAwaitableOfTSymbol { get; }

public INamedTypeSymbol? ConfigureAwaitOptionsSymbol { get; }
public int? SuppressThrowingValue { get; }

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

public void AnalyzeAwait(OperationAnalysisContext context)
Expand All @@ -77,8 +83,13 @@ public void AnalyzeAwait(OperationAnalysisContext context)
context.ReportDiagnostic(Rule, operation);
}
}
else if (operation.Operation is IInvocationOperation { Instance: IAwaitOperation { Operation.Type: INamedTypeSymbol childAwaitOperationType }, Type: var invocationType } && invocationType.IsEqualToAny(ConfiguredTaskAwaitableSymbol, ConfiguredTaskAwaitableOfTSymbol))
else if (operation.Operation is IInvocationOperation { Instance: IAwaitOperation { Operation.Type: INamedTypeSymbol childAwaitOperationType } } invocation && invocation.Type.IsEqualToAny(ConfiguredTaskAwaitableSymbol, ConfiguredTaskAwaitableOfTSymbol))
{
// The inner task is the only one configured with SuppressThrowing, whereas Unwrap() would also
// suppress the exceptions of the outer task, which the outer await currently propagates
if (MaySuppressThrowing(invocation))
return;

// Task<Task>
if (childAwaitOperationType.IsEqualTo(TaskOfTaskSymbol))
{
Expand All @@ -91,5 +102,22 @@ public void AnalyzeAwait(OperationAnalysisContext context)
}
}
}

private bool MaySuppressThrowing(IInvocationOperation operation)
{
if (SuppressThrowingValue is not { } suppressThrowing)
return false;

foreach (var argument in operation.Arguments)
{
if (!argument.Parameter!.Type.IsEqualTo(ConfigureAwaitOptionsSymbol))
continue;

// The options are only known not to suppress the exceptions when they are a constant
return argument.Value.ConstantValue is not { HasValue: true, Value: int options } || (options & suppressThrowing) is not 0;
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using CodeFixTest = Meziantou.Analyzer.Test.Harness.CSharpCodeFixTest<
Meziantou.Analyzer.Rules.UseTaskUnwrapAnalyzer,
Expand Down Expand Up @@ -158,6 +158,70 @@ public Task TaskOfTask_ConfigureAwait_Root_WithoutUsingDirective()
return test.RunAsync();
}

[Fact]
public Task TaskOfTask_ConfigureAwaitOptions_Root()
{
var test = CreateTest();
test.TestCode = """
using System.Threading.Tasks;

Task<Task> a = null;
{|MA0152:await (await a).ConfigureAwait(ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.ContinueOnCapturedContext)|};
""";
test.FixedCode = """
using System.Threading.Tasks;

Task<Task> a = null;
await a.Unwrap().ConfigureAwait(ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.ContinueOnCapturedContext);
""";

return test.RunAsync();
}

[Fact]
public Task TaskOfTask_ConfigureAwaitOptions_SuppressThrowing_Root()
{
// Unwrap() would also suppress the exceptions of the outer task
var test = CreateTest();
test.TestCode = """
using System.Threading.Tasks;

Task<Task> a = null;
await (await a).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
""";

return test.RunAsync();
}

[Fact]
public Task TaskOfTask_ConfigureAwaitOptions_SuppressThrowing_Combined_Root()
{
var test = CreateTest();
test.TestCode = """
using System.Threading.Tasks;

Task<Task> a = null;
await (await a).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing | ConfigureAwaitOptions.ForceYielding);
""";

return test.RunAsync();
}

[Fact]
public Task TaskOfTask_ConfigureAwaitOptions_NotConstant_Root()
{
var test = CreateTest();
test.TestCode = """
using System.Threading.Tasks;

Task<Task> a = null;
ConfigureAwaitOptions options = default;
await (await a).ConfigureAwait(options);
""";

return test.RunAsync();
}

[Fact]
public Task TaskOfTask_Unwrap_ConfigureAwait_Root()
{
Expand Down