Files
icsharpcode-ILSpy/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncAwaitPatterns.cs
T
dualfroz 3f1ef717b6 Fix #4017: Look through LdObjIfRef on the operand of await
A constrained GetAwaiter call on a type parameter that is not known to be
a value type wraps its receiver in LdObjIfRef so the reference is read
before any further arguments. ExpressionBuilder has no translation for
LdObjIfRef, so the operand was dropped. GetAwaiter takes no further
arguments, so the operand can be translated from the address, as
CallBuilder already does for call receivers.
2026-10-16 10:07:54 +02:00

720 lines
14 KiB
C#

// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// An await shape that still decompiles to code that does not compile is tracked as #4019 (with
// expression); it is absent here because it has no correct output to pin yet.
#pragma warning disable 1998
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.AsyncAwait
{
public class AwaitableContainer
{
public class NestedAwaitable
{
public TaskAwaiter GetAwaiter()
{
#if CS71
return default;
#else
return default(TaskAwaiter);
#endif
}
}
}
/// <summary>
/// The context the await sits in: how the translated expression has to be parenthesized, and
/// where the async state machine splits the surrounding statement.
/// </summary>
public class AwaitContexts
{
#if CS80 && !NET40
private sealed class AsyncDisposable : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
return default;
}
}
#endif
private static Task<int> Get()
{
return Task.FromResult(1);
}
private static Task<string> GetString()
{
return Task.FromResult("s");
}
private static Task<Exception> GetException()
{
return Task.FromResult(new Exception());
}
public async Task Statement()
{
await Get();
}
public async Task Argument()
{
Console.WriteLine(await Get());
}
#if ROSLYN2 || OPT
public async Task BinaryOperator()
{
Console.WriteLine(await Get() + await Get());
}
#endif
#if ROSLYN2 || OPT
public async Task UnaryOperator()
{
Console.WriteLine(-(await Get()));
}
#endif
#if ROSLYN2 || OPT
public async Task MemberAccessOnResult()
{
Console.WriteLine((await GetString()).Length);
}
#endif
#if ROSLYN2 || OPT
public async Task IndexerOnResult()
{
Console.WriteLine((await GetString())[0]);
}
#endif
#if ROSLYN2 || OPT
public async Task CoalesceOnResult()
{
Console.WriteLine((await GetString()) ?? "null");
}
#endif
public async Task ThrowAwaitedException()
{
throw await GetException();
}
#if ROSLYN2 || OPT
public async Task Checked()
{
Console.WriteLine(checked(await Get() + 1));
}
#endif
#if CS60
public async Task TryFinally()
{
try
{
await Get();
}
finally
{
await Get();
}
}
#endif
public async Task Using()
{
using (new Disposable())
{
await Get();
}
}
#if CS60
public async Task ConditionalAccessOnResult()
{
#if ROSLYN2 || OPT
Console.WriteLine((await GetString())?.Length);
#else
object value = (await GetString())?.Length;
Console.WriteLine(value);
#endif
}
public async Task CatchWithFilter()
{
try
{
await Get();
}
catch (Exception ex) when (ex.Message.Length > 2)
{
await Get();
}
}
#endif
#if CS70 && !NET40
public async Task AwaitInTupleLiteral()
{
Console.WriteLine((await Get(), await GetString()));
}
#endif
#if CS80 && !NET40
public async Task AwaitUsing()
{
await using (new AsyncDisposable())
{
await Get();
}
}
public async Task AwaitForeach(IAsyncEnumerable<int> source)
{
await foreach (int item in source)
{
Console.WriteLine(item);
}
}
public async Task AwaitForeachConfigured(IAsyncEnumerable<int> source)
{
await foreach (int item in source.ConfigureAwait(continueOnCapturedContext: false))
{
Console.WriteLine(item);
}
}
public async IAsyncEnumerable<int> AsyncIterator()
{
yield return await Get();
await Task.Yield();
yield return 2;
}
public async IAsyncEnumerable<int> AsyncIteratorWithFinally()
{
try
{
yield return await Get();
}
finally
{
Console.WriteLine("cleanup");
}
}
public async Task LocalFunction()
{
Console.WriteLine(await Local());
static async Task<int> Local()
{
return await Get();
}
}
#endif
#if ROSLYN2 || OPT
public async Task AwaitInGenericMethod<T>(Task<T> task)
{
Console.WriteLine(await task);
}
#endif
}
public static class AwaiterExtensions
{
public static TaskAwaiter GetAwaiter(this IAwaitableMarker marker)
{
#if CS71
return default;
#else
return default(TaskAwaiter);
#endif
}
public static TaskAwaiter GetAwaiter(this int millisecondsDelay)
{
return Task.Delay(millisecondsDelay).GetAwaiter();
}
public static TaskAwaiter GetAwaiter(this Action action)
{
#if CS71
return default;
#else
return default(TaskAwaiter);
#endif
}
public static TaskAwaiter<T[]> GetAwaiter<T>(this IEnumerable<Task<T>> tasks)
{
#if CS71
return default;
#else
return default(TaskAwaiter<T[]>);
#endif
}
#if CS70 && !NET40
public static TaskAwaiter<T> GetAwaiter<T>(this (Task<T>, string) taggedTask)
{
return taggedTask.Item1.GetAwaiter();
}
#endif
#if CS72
public static TaskAwaiter GetAwaiter(this in ByRefReceiver receiver)
{
return receiver.Self();
}
#endif
}
/// <summary>
/// The operand side: the shape of the expression the await is applied to.
/// </summary>
public class AwaitOperands
{
private Task<int> taskField;
private StructAwaitable structField;
private readonly StructAwaitable readonlyStructField;
private Task<int> Property {
get {
Console.WriteLine("get_Property");
return taskField;
}
}
private Task<int> this[int index] {
get {
Console.WriteLine("get_Item");
return taskField;
}
}
private static Task<int> Get()
{
return Task.FromResult(1);
}
public async Task DefaultOfStruct()
{
await default(StructAwaitable);
}
public async Task Ternary(bool condition, Task first, Task second)
{
await (condition ? first : second);
}
public async Task Coalesce(Task first, Task second)
{
await (first ?? second);
}
#if CS60
public async Task NullConditional(List<Task> tasks)
{
await (tasks?[0]);
}
#endif
public async Task Cast(object obj)
{
await (Task)obj;
}
/// <summary>
/// A null literal has no type, so the cast is what makes the operand awaitable. Note that
/// <c>default(Task)</c> compiles to the same `ldnull` and therefore decompiles to this same
/// cast; the two are indistinguishable in IL.
/// </summary>
public async Task NullLiteral()
{
await (Task)null;
}
public async Task AsOperator(object obj)
{
await (obj as Task);
}
public async Task FieldAccess()
{
Console.WriteLine(await taskField);
}
public async Task PropertyAccess()
{
Console.WriteLine(await Property);
}
public async Task IndexerAccess()
{
Console.WriteLine(await this[0]);
}
public async Task StructField()
{
await structField;
}
public async Task ReadOnlyStructField()
{
await readonlyStructField;
}
public async Task StructArrayElement(StructAwaitable[] awaitables)
{
await awaitables[0];
}
public async Task MethodCall()
{
Console.WriteLine(await Get());
}
public async Task DelegateInvocation(Func<Task<int>> factory)
{
Console.WriteLine(await factory());
}
public async Task ArrayElement(Task<int>[] tasks)
{
Console.WriteLine(await tasks[0]);
}
public async Task TernaryOfTasks(bool condition)
{
Console.WriteLine(await (condition ? Get() : Get()));
}
}
/// <summary>
/// The receiver ("expected type") side of ExpressionBuilder.VisitAwait: the awaited expression
/// is converted to the declaring type of the resolved GetAwaiter, or to its first parameter
/// type when GetAwaiter is an extension method.
/// </summary>
public class AwaitReceivers
{
public async Task InstanceAwaiterOnSelf(ClassAwaitable awaitable)
{
await awaitable;
}
public async Task AwaiterInheritedFromBaseClass(DerivedAwaitable awaitable)
{
await awaitable;
}
public async Task AwaiterThroughInterface(IAwaitable awaitable)
{
await awaitable;
}
public async Task AwaiterThroughBaseInterface(IDerivedAwaitable awaitable)
{
await awaitable;
}
/// <summary>
/// The cast carries the operand to the interface that declares GetAwaiter; without it the
/// explicit implementation is not accessible. A conversion that is merely implicit must not
/// be dropped here, even though a boxing conversion exists.
/// </summary>
public async Task ExplicitInterfaceImplementationOnStruct(ExplicitStructAwaitable awaitable)
{
await (IAwaitable)awaitable;
}
/// <summary>
/// The same shape on a class, i.e. it is not specific to the boxing conversion.
/// </summary>
public async Task ExplicitInterfaceImplementationOnClass(ExplicitClassAwaitable awaitable)
{
await (IAwaitable)awaitable;
}
/// <summary>
/// The await pattern does not apply user-defined conversions, so the cast that invokes
/// op_Implicit has to survive.
/// </summary>
public async Task UserDefinedConversionToAwaitable(ConvertsToAwaitable awaitable)
{
await (ClassAwaitable)awaitable;
}
public async Task ExtensionAwaiterOnClass(MarkerClass marker)
{
await marker;
}
public async Task ExtensionAwaiterOnStruct(MarkerStruct marker)
{
await marker;
}
public async Task ExtensionAwaiterOnPrimitive()
{
await 100;
}
public async Task ExtensionAwaiterOnDelegate(Action action)
{
await action;
}
#if CS72
/// <summary>
/// An extension GetAwaiter taking its receiver by 'in' makes the expected type a
/// ByReferenceType. Stripping the 'ref' must not leave a conversion that reaches the
/// managed reference back through a pointer.
/// </summary>
public async Task InReceiverExtensionAwaiter(ByRefReceiver receiver)
{
await receiver;
}
#endif
#if ROSLYN2
public async Task ExtensionAwaiterOverTaskArray(Task<int>[] tasks)
{
Console.WriteLine((await tasks)[0]);
}
#endif
#if ROSLYN2
public async Task ExtensionAwaiterOverTaskList(List<Task<int>> tasks)
{
Console.WriteLine((await tasks)[0]);
}
#endif
public async Task GenericAwaitableType(GenericAwaitable<string> awaitable)
{
Console.WriteLine(await awaitable);
}
public async Task NestedAwaitableType(AwaitableContainer.NestedAwaitable awaitable)
{
await awaitable;
}
public async Task TypeParameterWithClassConstraint<T>(T awaitable) where T : ClassAwaitable
{
await awaitable;
}
public async Task TypeParameterWithStructConstraint<T>(T awaitable) where T : struct, IAwaitable
{
await awaitable;
}
public async Task TypeParameterWithInterfaceConstraint<T>(T awaitable) where T : IAwaitable
{
await awaitable;
}
public async Task TypeParameterArrayElementWithInterfaceConstraint<T>(T[] awaitables) where T : IAwaitable
{
await awaitables[0];
}
public async Task ConfiguredTaskAwaitable(Task<int> task)
{
#if ROSLYN2
Console.WriteLine(await task.ConfigureAwait(continueOnCapturedContext: false));
#else
Console.WriteLine(await task.ConfigureAwait(false));
#endif
}
#if CS70 && !NET40
public async Task ExtensionAwaiterOnTuple(Task<int> task)
{
Console.WriteLine(await (task, "tag"));
}
#endif
#if CS80 && !NET40
public async Task ValueTaskAwaitable(ValueTask<int> task)
{
Console.WriteLine(await task);
}
public async Task ConfiguredValueTaskAwaitable(ValueTask<int> task)
{
Console.WriteLine(await task.ConfigureAwait(continueOnCapturedContext: false));
}
#endif
#if NET80
public async Task ConfigureAwaitWithOptions(Task task)
{
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
#endif
public async Task AwaitOfAwait(Task<Task<int>> task)
{
Console.WriteLine(await (await task));
}
}
#if CS72
public struct ByRefReceiver
{
public long A;
public long B;
public TaskAwaiter Self()
{
#if CS71
return default;
#else
return default(TaskAwaiter);
#endif
}
}
#endif
public class ClassAwaitable : IAwaitable
{
public TaskAwaiter GetAwaiter()
{
#if CS71
return default;
#else
return default(TaskAwaiter);
#endif
}
}
public class ConvertsToAwaitable
{
public static implicit operator ClassAwaitable(ConvertsToAwaitable value)
{
return new ClassAwaitable();
}
}
public class DerivedAwaitable : ClassAwaitable
{
}
public class Disposable : IDisposable
{
public void Dispose()
{
}
}
public class ExplicitClassAwaitable : IAwaitable
{
TaskAwaiter IAwaitable.GetAwaiter()
{
#if CS71
return default;
#else
return default(TaskAwaiter);
#endif
}
}
[StructLayout(LayoutKind.Sequential, Size = 1)]
public struct ExplicitStructAwaitable : IAwaitable
{
TaskAwaiter IAwaitable.GetAwaiter()
{
#if CS71
return default;
#else
return default(TaskAwaiter);
#endif
}
}
public class GenericAwaitable<T>
{
public TaskAwaiter<T> GetAwaiter()
{
#if CS71
return default;
#else
return default(TaskAwaiter<T>);
#endif
}
}
public interface IAwaitable
{
TaskAwaiter GetAwaiter();
}
public interface IAwaitableMarker
{
}
public interface IBaseAwaitable
{
TaskAwaiter GetAwaiter();
}
public interface IDerivedAwaitable : IBaseAwaitable
{
}
public class MarkerClass : IAwaitableMarker
{
}
[StructLayout(LayoutKind.Sequential, Size = 1)]
public struct MarkerStruct : IAwaitableMarker
{
}
public struct StructAwaitable : IAwaitable
{
public int Counter;
public TaskAwaiter GetAwaiter()
{
Counter++;
#if CS71
return default;
#else
return default(TaskAwaiter);
#endif
}
}
}