diff --git a/docs/Rules/MA0152.md b/docs/Rules/MA0152.md index baac1ccfd..46d33f1d5 100644 --- a/docs/Rules/MA0152.md +++ b/docs/Rules/MA0152.md @@ -1,4 +1,4 @@ -# MA0152 - Use Unwrap instead of using await twice +# MA0152 - Use Unwrap instead of using await twice 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) @@ -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 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); +```` diff --git a/src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs index 48739f5f6..daec1872d 100644 --- a/src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs @@ -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().FirstOrDefault()?.ConstantValue as int?; + if (TaskSymbol is not null && TaskOfTSymbol is not null) { TaskOfTaskSymbol = TaskOfTSymbol.Construct(TaskSymbol); @@ -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) @@ -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 if (childAwaitOperationType.IsEqualTo(TaskOfTaskSymbol)) { @@ -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; + } } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs index db2733530..f67f9b0ae 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs @@ -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, @@ -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 a = null; + {|MA0152:await (await a).ConfigureAwait(ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.ContinueOnCapturedContext)|}; + """; + test.FixedCode = """ + using System.Threading.Tasks; + + 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 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 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 a = null; + ConfigureAwaitOptions options = default; + await (await a).ConfigureAwait(options); + """; + + return test.RunAsync(); + } + [Fact] public Task TaskOfTask_Unwrap_ConfigureAwait_Root() {