Compare commits

..
10 Commits
Author SHA1 Message Date
dualfroz 64a20c44d9 Describe this repository 2026-10-22 21:04:18 +02:00
dualfroz 85285c1cea Fix #4019: Recognize with expressions whose initializer awaits
Roslyn spills the receiver of an initializer assignment that precedes an
await into a hoisted temporary, so the clone of a with expression (and the
object of an object initializer) reaches the initializer transform through
a copy it rejects. Only copies between two hoisted compiler temporaries are
inlined: folding copies of hoisted user locals as well would turn a user's
local into an initializer.
2026-10-10 17:52:06 +02:00
upstream ef47a24511 Upstream icsharpcode/ILSpy state of the touched files before b3c37fd8d4 2026-10-10 13:18:39 +02:00
dualfroz 72e7db13fa #4136: Simplify bool-typed if (c) 1 else 0 to c != 0
An IfInstruction tests its condition for non-zero, so a bool-typed
`if (c) ldc.i4 1 else ldc.i4 0` always equals `c != 0`, which
ExpressionTransforms already folds to `c` for bool `c`. Without this
identity the node reached the C# output as `c ? true : false`, or as
`c || 1 == 0` once negated. The lost `??` form is not addressed here.
2026-10-10 13:50:53 +02:00
upstream 6e2842992c Upstream icsharpcode/ILSpy state of the touched files before 29888045ca 2026-10-10 13:07:54 +02:00
dualfroz ffe7e7b3f1 Fix #3702: Don't rewrite GetValueOrDefault() to == true in ?. chains
Turning bool?.GetValueOrDefault() into `== true` ends an enclosing
null-conditional chain, so `c?.Flag.GetValueOrDefault()` became
`(c?.Flag == true)` of type bool instead of bool?. A following `?? false`
then fails to compile (CS0019), and a following `.ToString()` yields
"False" instead of null when c is null. Keep the call when its target is
part of a ?. chain.
2026-10-16 10:07:54 +02:00
upstream 8e3e41fedb Upstream icsharpcode/ILSpy state of the touched files before 53822318d9 2026-10-10 13:03:54 +02:00
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
upstream 587f5c9583 Upstream icsharpcode/ILSpy state of the touched files before c5f455e56b 2026-10-10 13:01:51 +02:00
dualfroz 999d3b48fa Fix #4137: Keep nint in conditionals with mixed native int branches
When the branches of a conditional have different types, its type falls
back to the IL stack slot type, which is (U)IntPtr. IntroduceNativeIntTypeOnLocals
only retypes variables, so with native integers enabled the branches got
cast to IntPtr. Map (U)IntPtr to n(u)int the same way boxing does; pointer
types are unaffected.
2026-10-10 13:50:52 +02:00
13 changed files with 5860 additions and 37 deletions

No files matched your search

@@ -0,0 +1,85 @@
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
{
class NullPropagation
{
static void Main()
{
new NullPropagation().TestNotCoalescing();
new NullPropagation().TestNullableBoolInChain();
#if CS72
new NullPropagation().TestRefStructNotCoalescing();
#endif
}
class MyClass
{
public string Text;
public bool? Flag;
}
void TestNotCoalescing()
{
Console.WriteLine("TestNotCoalescing:");
Console.WriteLine(NotCoalescing(null));
Console.WriteLine(NotCoalescing(new MyClass()));
}
string NotCoalescing(MyClass c)
{
return c != null ? c.Text : "Hello";
}
void TestNullableBoolInChain()
{
Console.WriteLine("TestNullableBoolInChain:");
foreach (MyClass c in new[] { null, new MyClass(), new MyClass { Flag = false }, new MyClass { Flag = true } })
{
Console.WriteLine(EqualsTrue(c));
Console.WriteLine(GetValueOrDefaultToString(c) ?? "<null>");
}
}
bool EqualsTrue(MyClass c)
{
return c?.Flag == true;
}
string GetValueOrDefaultToString(MyClass c)
{
return c?.Flag.GetValueOrDefault().ToString();
}
#if CS72
ref struct ByRefLikeStruct
{
public int Value;
}
class RefStructHolder
{
public ByRefLikeStruct Get()
{
ByRefLikeStruct result = default(ByRefLikeStruct);
result.Value = 42;
return result;
}
}
void TestRefStructNotCoalescing()
{
Console.WriteLine("TestRefStructNotCoalescing:");
Console.WriteLine(RefStructNotCoalescing(null).Value);
Console.WriteLine(RefStructNotCoalescing(new RefStructHolder()).Value);
}
// A by-ref-like return type cannot be turned into a ?. / ?? null-propagation: a ref struct
// cannot be wrapped in Nullable<T>, so that form does not compile.
ByRefLikeStruct RefStructNotCoalescing(RefStructHolder holder)
{
return holder != null ? holder.Get() : default(ByRefLikeStruct);
}
#endif
}
}
@@ -0,0 +1,710 @@
// 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 #4017 (type
// parameter with an interface constraint); 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 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
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -254,5 +254,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
// Explicit `unchecked` is necessary when casting oversized constant to nuint
return unchecked((nuint)9123123123123uL);
}
public nint ConditionalWithMixedBranchTypes(bool b, int i, nuint x, nint y)
{
return (b ? ((nint)i) : ((nint)x)) - y;
}
}
}
@@ -0,0 +1,395 @@
// Copyright (c) 2018 Daniel Grunwald
//
// 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.
using System;
using System.Collections;
using System.Collections.Generic;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class NullPropagation
{
private class MyClass
{
public int IntVal;
public readonly int ReadonlyIntVal;
public MyStruct StructField;
public readonly MyStruct ReadonlyStructField;
public string Text;
public MyClass Field;
public bool? NullableBool;
public MyClass Property { get; set; }
public MyClass this[int index] => null;
public MyClass Method(int arg)
{
return null;
}
public bool GetBool()
{
return false;
}
public void Done()
{
}
}
private struct MyStruct
{
public int IntVal;
public readonly int ReadonlyIntVal;
public MyClass Field;
public MyStruct? Property1 => null;
#if CS71
public MyStruct Property2 => default;
#else
public MyStruct Property2 => default(MyStruct);
#endif
public MyStruct? this[int index] => null;
public MyStruct? Method1(int arg)
{
return null;
}
public MyStruct Method2(int arg)
{
#if CS71
return default;
#else
return default(MyStruct);
#endif
}
public bool GetBool()
{
return false;
}
public void Done()
{
}
}
private class Container<T1, T2>
{
public GenericStruct<T1, T2> Other;
}
private struct GenericStruct<T1, T2>
{
public T1 Field1;
public T2 Field2;
public Container<T1, T2> Other;
public override string ToString()
{
return "(" + Field1?.ToString() + ", " + Field2?.ToString() + ")";
}
public int? GetTextLength()
{
return Field1?.ToString().Length + Field2?.ToString().Length + 4;
}
public string Chain1()
{
return Other?.Other.Other?.Other.Field1?.ToString();
}
public string Chain2()
{
return Other?.Other.Other?.Other.Field1?.ToString()?.GetType().Name;
}
public int? Test2()
{
return Field1?.ToString().Length ?? 42;
}
public int? GetTextLengthNRE()
{
return (Field1?.ToString()).Length;
}
}
public interface ITest
{
int Int();
ITest Next();
}
private int GetInt()
{
return 9;
}
private string GetString()
{
return null;
}
private MyClass GetMyClass()
{
return null;
}
private MyStruct? GetMyStruct()
{
return null;
}
public string Substring()
{
return GetString()?.Substring(GetInt());
}
public void CallSubstringAndIgnoreResult()
{
GetString()?.Substring(GetInt());
}
private void Use<T>(T t)
{
}
public void CallDone()
{
GetMyClass()?.Done();
GetMyClass()?.Field?.Done();
GetMyClass()?.Field.Done();
GetMyClass()?.Property?.Done();
GetMyClass()?.Property.Done();
GetMyClass()?.Method(GetInt())?.Done();
GetMyClass()?.Method(GetInt()).Done();
GetMyClass()?[GetInt()]?.Done();
GetMyClass()?[GetInt()].Done();
}
public void CallGetBool()
{
GetMyClass()?.GetBool();
GetMyClass()?.Field?.GetBool();
GetMyClass()?.Field.GetBool();
GetMyClass()?.Property?.GetBool();
GetMyClass()?.Property.GetBool();
GetMyClass()?.Method(GetInt())?.GetBool();
GetMyClass()?.Method(GetInt()).GetBool();
GetMyClass()?[GetInt()]?.GetBool();
GetMyClass()?[GetInt()].GetBool();
}
public void CallDoneStruct()
{
GetMyStruct()?.Done();
GetMyStruct()?.Field?.Done();
GetMyStruct()?.Field.Done();
GetMyStruct()?.Property1?.Done();
GetMyStruct()?.Property2.Done();
GetMyStruct()?.Method1(GetInt())?.Done();
GetMyStruct()?.Method2(GetInt()).Done();
GetMyStruct()?[GetInt()]?.Done();
}
public void CallGetBoolStruct()
{
GetMyStruct()?.GetBool();
GetMyStruct()?.Field?.GetBool();
GetMyStruct()?.Field.GetBool();
GetMyStruct()?.Property1?.GetBool();
GetMyStruct()?.Property2.GetBool();
GetMyStruct()?.Method1(GetInt())?.GetBool();
GetMyStruct()?.Method2(GetInt()).GetBool();
GetMyStruct()?[GetInt()]?.GetBool();
}
public void RequiredParentheses()
{
(GetMyClass()?.Field).Done();
(GetMyClass()?.Method(GetInt())).Done();
(GetMyStruct()?.Property2)?.Done();
}
public int?[] ChainsOnClass()
{
return new int?[9] {
GetMyClass()?.IntVal,
GetMyClass()?.Field.IntVal,
GetMyClass()?.Field?.IntVal,
GetMyClass()?.Property.IntVal,
GetMyClass()?.Property?.IntVal,
GetMyClass()?.Method(GetInt()).IntVal,
GetMyClass()?.Method(GetInt())?.IntVal,
GetMyClass()?[GetInt()].IntVal,
GetMyClass()?[GetInt()]?.IntVal
};
}
public int?[] ChainsStruct()
{
return new int?[8] {
GetMyStruct()?.IntVal,
GetMyStruct()?.Field.IntVal,
GetMyStruct()?.Field?.IntVal,
GetMyStruct()?.Property2.IntVal,
GetMyStruct()?.Property1?.IntVal,
GetMyStruct()?.Method2(GetInt()).IntVal,
GetMyStruct()?.Method1(GetInt())?.IntVal,
GetMyStruct()?[GetInt()]?.IntVal
};
}
public int CoalescingReturn()
{
return GetMyClass()?.IntVal ?? 1;
}
public void Coalescing()
{
Use(GetMyClass()?.IntVal ?? 1);
}
public void CoalescingString()
{
Use(GetMyClass()?.Text ?? "Hello");
}
public void CallOnValueTypeField()
{
Use(GetMyClass()?.IntVal.ToString());
Use(GetMyStruct()?.IntVal.ToString());
Use(GetMyClass()?.ReadonlyIntVal.ToString());
Use(GetMyStruct()?.ReadonlyIntVal.ToString());
GetMyClass()?.StructField.Done();
GetMyClass()?.ReadonlyStructField.Done();
}
public void InvokeDelegate(EventHandler eh)
{
eh?.Invoke(null, EventArgs.Empty);
}
public int? InvokeDelegate(Func<int> f)
{
return f?.Invoke();
}
private void NotNullPropagation(MyClass c)
{
// don't decompile this to "(c?.IntVal ?? 0) != 0"
if (c != null && c.IntVal != 0)
{
Console.WriteLine("non-zero");
}
if (c == null || c.IntVal == 0)
{
Console.WriteLine("null or zero");
}
Console.WriteLine("end of method");
}
private void Setter(MyClass c)
{
if (c != null)
{
c.IntVal = 1;
}
Console.WriteLine();
if (c != null)
{
c.Property = null;
}
}
private static int? GenericUnconstrainedInt<T>(T t) where T : ITest
{
return t?.Int();
}
private static int? GenericClassConstraintInt<T>(T t) where T : class, ITest
{
return t?.Int();
}
private static int? GenericStructConstraintInt<T>(T? t) where T : struct, ITest
{
return t?.Int();
}
private static int? GenericRefUnconstrainedInt<T>(ref T t) where T : ITest
{
return t?.Int();
}
private static int? GenericRefClassConstraintInt<T>(ref T t) where T : class, ITest
{
return t?.Int();
}
private static int? GenericRefStructConstraintInt<T>(ref T? t) where T : struct, ITest
{
return t?.Int();
}
public int? Issue1709(object obj)
{
return (obj as ICollection)?.Count + (obj as ICollection<int>)?.Count;
}
private static void Issue1689(List<byte[]> setsOfNumbers)
{
Console.WriteLine(setsOfNumbers?[0]?[1].ToString() == "2");
Console.WriteLine(setsOfNumbers?[1]?[1].ToString() == null);
}
private static byte[] GetBytes()
{
return null;
}
private static void ArrayLengthWithFallback()
{
Console.WriteLine(GetBytes()?.Length ?? 0);
Console.WriteLine(GetBytes()?.LongLength ?? 0);
}
private static dynamic DynamicNullProp(dynamic a)
{
return a?.b.c(1)?.d[10];
}
private static string Issue3181()
{
#if EXPECTED_OUTPUT
return ((int?)null)?.ToString();
#else
int? x = null;
return x?.ToString();
#endif
}
private static bool Issue3702(MyClass c)
{
return c?.NullableBool.GetValueOrDefault() ?? false;
}
private static string Issue3702ToString(MyClass c)
{
return c?.NullableBool.GetValueOrDefault().ToString();
}
}
}
@@ -0,0 +1,546 @@
using System;
#if CS100
using System.Runtime.InteropServices;
using System.Text;
#endif
using System.Threading.Tasks;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class RecordClasses
{
public record Base(string A);
public record CopyCtor(string A)
{
protected CopyCtor(CopyCtor _)
{
}
}
public record Derived(int B) : Base(B.ToString());
public record BaseRecordWithObject(object Id);
public record DerivedRecordWithString : BaseRecordWithObject
{
public DerivedRecordWithString(string Id)
: base(Id)
{
}
}
public record Empty;
public record EmptyWithStaticField
{
public static readonly Empty X = new Empty();
}
public record Fields
{
public int A;
public double B = 1.0;
public object C;
public dynamic D;
public string S = "abc";
}
public record Interface(int B) : IRecord;
public interface IRecord
{
}
public record Pair<A, B>
{
public A First { get; init; }
public B Second { get; init; }
}
public record PairWithPrimaryCtor<A, B>(A First, B Second);
public record PrimaryCtor(int A, string B);
public record PrimaryCtorWithAttribute([RecordTest("param")][property: RecordTest("property")][field: RecordTest("field")] int a);
public record PrimaryCtorWithField(int A, string B)
{
public double C = 1.0;
public string D = A + B;
}
public record PrimaryCtorWithInParameter(in int A, in string B);
public record PrimaryCtorWithProperty(int A, string B)
{
public double C { get; init; } = 1.0;
public string D { get; } = A + B;
}
public record Properties
{
public int A { get; set; }
public int B { get; }
public int C => 43;
public object O { get; set; }
public string S { get; set; }
public dynamic D { get; set; }
public Properties()
{
B = 42;
}
}
#if CS110
public record WithRequiredMembers
{
public required Type ServiceType { get; init; }
public required object ServiceKey { get; init; }
}
#endif
[AttributeUsage(AttributeTargets.All)]
public class RecordTestAttribute : Attribute
{
public RecordTestAttribute(string name)
{
}
}
public sealed record Sealed(string A);
public sealed record SealedDerived(int B) : Base(B.ToString());
public class WithExpressionTests
{
public Fields Test(Fields input)
{
return input with {
A = 42,
B = 3.141,
C = input
};
}
public Fields Test2(Fields input)
{
return input with {
A = 42,
B = 3.141,
C = input with {
A = 43
}
};
}
public async Task<PrimaryCtor> WithAwait(PrimaryCtor input)
{
return input with {
A = await Task.FromResult(42)
};
}
public async Task<Fields> WithAwaitAfterConstant(Fields input)
{
return input with {
A = 42,
B = await Task.FromResult(3.141)
};
}
}
public abstract record WithNestedRecords
{
public record A : WithNestedRecords
{
public override string AbstractProp => "A";
}
public record B : WithNestedRecords
{
public override string AbstractProp => "B";
public int? Value { get; set; }
}
public record DerivedGeneric<T> : Pair<T, T?> where T : struct
{
public bool Flag;
}
public abstract string AbstractProp { get; }
}
public abstract record BaseRecord
{
public string Name { get; }
public object Value { get; }
public bool Encode { get; }
protected BaseRecord(string name, object value, bool encode)
{
Name = name;
Value = value;
Encode = encode;
}
}
public record DerivedRecord(string name, object value, bool encode = true) : BaseRecord(string.IsNullOrEmpty(name) ? "name" : name, value, encode);
public record DefaultValuesRecord() : DerivedRecord("default", 42, encode: false);
public record RecordWithProtectedMember(int Value)
{
protected int Double => Value * 2;
}
public record InheritedRecordWithAdditionalMember(int Value) : RecordWithProtectedMember(Value)
{
public int MoreData { get; set; }
}
public record InheritedRecordWithAdditionalParameter(int Value, int Value2) : RecordWithProtectedMember(Value);
public record BaseWithString(string S);
public record DerivedWithAdditionalInt(int I) : BaseWithString(I.ToString());
public record DerivedWithNoAdditionalProperty(string S) : BaseWithString(S);
public record DerivedWithAdditionalProperty(string S2) : BaseWithString(S2);
public record DerivedWithAdditionalPropertyDifferentAccessor(string S) : BaseWithString(S)
{
public string S2 { get; set; } = S;
}
public record MultipleCtorsChainedNoPrimaryCtor
{
public int A { get; init; }
public string B { get; init; }
public double C { get; init; }
public MultipleCtorsChainedNoPrimaryCtor(double c)
{
A = 0;
B = null;
C = c;
}
public MultipleCtorsChainedNoPrimaryCtor(int a)
: this(3.14)
{
A = a;
}
public MultipleCtorsChainedNoPrimaryCtor(string b)
: this(4.13)
{
B = b;
}
}
public record UnexpectedCodeInCtor
{
public int A { get; init; }
public string B { get; init; }
public UnexpectedCodeInCtor(int A, string B)
{
this.A = A;
this.B = B;
Console.WriteLine();
}
public UnexpectedCodeInCtor(int A)
: this(A, null)
{
this.A = A;
}
}
private record RecordWithMultipleInitializerAssignmentsInPrimaryCtor(string name, string? value, in object encode)
{
public string? Value { get; } = name;
public string Name { get; } = value;
private string? WebValue { get; } = (name != null) ? "111" : value;
private string? WebValue2;
}
public record FieldBeforeProperty(int ID, string Text)
{
public int Field;
public int Property { get; set; }
}
public record FieldsAndPropertiesInterleaved(int ID)
{
public int First;
public int Middle { get; set; }
public int Last;
}
public record PrivateFieldAndComputedProperty(int ID)
{
public int PublicField;
private int privateField;
public int Computed => privateField + PublicField;
public int Auto { get; set; }
}
public record DerivedWithInterleavedMembers(int B) : Base(B.ToString())
{
public int Field;
public int Property { get; set; }
}
}
#if CS100
internal class RecordStructs
{
public readonly record struct ReadonlyPoint(int X, int Y);
public record struct Base(string A);
public record CopyCtor(string A)
{
protected CopyCtor(CopyCtor _)
{
}
}
[StructLayout(LayoutKind.Sequential, Size = 1)]
public record struct Empty;
public record struct Fields
{
public int A;
public double B;
public object C;
public dynamic D;
public string S;
}
public record struct Interface(int B) : IRecord;
public interface IRecord
{
}
public record struct Pair<A, B>
{
public A First { get; init; }
public B Second { get; init; }
}
public record struct PairWithPrimaryCtor<A, B>(A First, B Second);
public readonly record struct BoundsInfo(int Profile, Bounds Bounds);
public readonly record struct Bounds(ulong Small, ulong Large);
public record struct PrimaryCtor(int A, string B);
public record struct MultipleCtorsNoPrimaryCtor
{
public Guid Id { get; }
public MultipleCtorsNoPrimaryCtor()
{
Id = Guid.NewGuid();
}
public MultipleCtorsNoPrimaryCtor(Guid id)
{
Id = id;
}
}
public record struct MultipleCtorsChainedNoPrimaryCtor
{
public int A { get; init; }
public string B { get; init; }
public double C { get; init; }
public MultipleCtorsChainedNoPrimaryCtor(double c)
{
A = 0;
B = null;
C = c;
}
public MultipleCtorsChainedNoPrimaryCtor(int a)
: this(3.14)
{
A = a;
}
public MultipleCtorsChainedNoPrimaryCtor(string b)
: this(4.13)
{
B = b;
}
}
public record struct PrimaryCtorWithAttribute([RecordTest("param")][property: RecordTest("property")][field: RecordTest("field")] int a);
public record struct PrimaryCtorWithField(int A, string B)
{
public double C = 1.0;
public string D = A + B;
}
public record struct PrimaryCtorWithInParameter(in int A, in string B);
public record struct PrimaryCtorWithProperty(int A, string B)
{
public double C { get; init; } = 1.0;
public string D { get; } = A + B;
}
public record struct Properties
{
public int A { get; set; }
public int B { get; }
public int C => 43;
public object O { get; set; }
public string S { get; set; }
public dynamic D { get; set; }
public Properties()
{
A = 41;
B = 42;
O = null;
S = "Hello";
D = null;
}
}
public record struct PropertiesWithInitializers()
{
public int A { get; set; } = 41;
public int B { get; } = 42;
public int C => 43;
public object O { get; set; } = null;
public string S { get; set; } = "Hello";
public dynamic D { get; set; } = null;
}
[AttributeUsage(AttributeTargets.All)]
public class RecordTestAttribute : Attribute
{
public RecordTestAttribute(string name)
{
}
}
public class WithExpressionTests
{
public Fields Test(Fields input)
{
return input with {
A = 42,
B = 3.141,
C = input
};
}
public Fields Test2(Fields input)
{
return input with {
A = 42,
B = 3.141,
C = input with {
A = 43
}
};
}
}
#if CS110
public record struct WithRequiredMembers
{
public int A { get; set; }
public required double B { get; set; }
public object C;
public required dynamic D;
}
#endif
public record struct RecordWithMultipleCtors
{
public int A { get; set; }
public RecordWithMultipleCtors()
{
A = 42;
}
public RecordWithMultipleCtors(int A)
{
this.A = A;
}
public RecordWithMultipleCtors(string A)
{
this.A = int.Parse(A);
}
}
public record struct RecordCtorChain(int A, string B)
{
#if EXPECTED_OUTPUT
public double C = 0.0;
#else
public double C;
#endif
public RecordCtorChain(int A)
: this(A, "default")
{
C = 3.14;
}
public RecordCtorChain(string B)
: this(42, B)
{
C = 1.41;
}
}
private static BoundsInfo GetInfo()
{
return new BoundsInfo(1, new Bounds(2uL, 3uL));
}
public static void NestedDeconstruction()
{
var (value, (value2, value3)) = GetInfo();
Console.WriteLine(value);
Console.WriteLine(value2);
Console.WriteLine(value3);
}
}
internal class RecordsWithCustomSynthesizedMembers
{
public record WithCustomToString(int A)
{
public sealed override string ToString()
{
return "custom";
}
}
public record WithCustomPrintMembers(int A)
{
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append("X");
return true;
}
}
}
#endif
}
+8 -16
View File
@@ -26,6 +26,7 @@ using System.Text;
using ICSharpCode.Decompiler.CSharp.Resolver;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.Semantics;
@@ -594,7 +595,8 @@ namespace ICSharpCode.Decompiler.CSharp
if (settings.LiftNullables && method.Name == "GetValueOrDefault"
&& method.DeclaringType.IsKnownType(KnownTypeCode.NullableOfT)
&& method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Boolean)
&& argumentList.Length == 0)
&& argumentList.Length == 0
&& !IntroduceQueryExpressions.IsNullConditional(target))
{
argumentList.CheckNoNamedOrOptionalArguments();
return new BinaryOperatorExpression(
@@ -1318,8 +1320,8 @@ namespace ICSharpCode.Decompiler.CSharp
return false;
var firstArgument = argumentList.Arguments[0];
bool canWriteTypeArguments = !settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType());
bool writeTypeArguments = (transform & ReferenceTransformation.RequireTypeArguments) != 0 && canWriteTypeArguments;
bool writeTypeArguments = (transform & ReferenceTransformation.RequireTypeArguments) != 0
&& (!settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType()));
IType[] typeArguments = writeTypeArguments ? method.TypeArguments.ToArray() : Empty<IType>.Array;
var directionExpression = firstArgument.Expression as DirectionExpression;
@@ -1342,20 +1344,10 @@ namespace ICSharpCode.Decompiler.CSharp
string[]? remainingNames = argumentNames?.Take(actualArgumentCount).Skip(1).ToArray();
if (remainingNames != null && remainingNames.All(name => name == null))
remainingNames = null;
var remainingArguments = argumentList.GetArgumentResolveResults(skipCount: 1).ToArray();
if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, remainingArguments, remainingNames))
if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target,
argumentList.GetArgumentResolveResults(skipCount: 1).ToArray(), remainingNames))
{
// 'a?.M()' has no static call form to fall back to. Spelled-out type arguments rule out
// extension methods of a different arity, which NullPropagationTransform relies on.
if (writeTypeArguments || !canWriteTypeArguments || method.TypeArguments.Count == 0
|| !IsNullConditional(firstArgument.Expression))
{
return false;
}
writeTypeArguments = true;
typeArguments = method.TypeArguments.ToArray();
if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, remainingArguments, remainingNames))
return false;
return false;
}
Expression targetExpression;
@@ -1825,6 +1825,22 @@ namespace ICSharpCode.Decompiler.CSharp
}
}
IType PreferNativeIntegerType(IType type)
{
if (settings.NativeIntegers)
{
if (type.IsKnownType(KnownTypeCode.IntPtr))
{
return SpecialType.NInt;
}
else if (type.IsKnownType(KnownTypeCode.UIntPtr))
{
return SpecialType.NUInt;
}
}
return type;
}
/// <summary>
/// Gets a type used for performing arithmetic with the stack type and sign.
///
@@ -3525,16 +3541,9 @@ namespace ICSharpCode.Decompiler.CSharp
{
IType targetType = inst.Type;
var arg = Translate(inst.Argument, typeHint: targetType);
if (settings.NativeIntegers && !arg.Type.Equals(targetType))
if (!arg.Type.Equals(targetType))
{
if (targetType.IsKnownType(KnownTypeCode.IntPtr))
{
targetType = SpecialType.NInt;
}
else if (targetType.IsKnownType(KnownTypeCode.UIntPtr))
{
targetType = SpecialType.NUInt;
}
targetType = PreferNativeIntegerType(targetType);
}
arg = arg.ConvertTo(targetType, this);
var obj = compilation.FindType(KnownTypeCode.Object);
@@ -3784,7 +3793,7 @@ namespace ICSharpCode.Decompiler.CSharp
{
var property = (IProperty)lastElement.Member;
Debug.Assert(property.Parameters.Count == indices);
Debug.Assert(property.Setter != null, $"Indexer property {property} has no setter");
DecompilerAssert.That(property.Setter != null, $"Indexer property {property} has no setter");
elementsStack.Peek().Add(
new CallBuilder(this, typeSystem, settings)
.BuildDictionaryInitializerExpression(lastElement.OpCode, property.Setter, initObjRR, GetIndices(lastElement.Indices, indexVariables).ToList(), info.Values!.Single())
@@ -4051,13 +4060,22 @@ namespace ICSharpCode.Decompiler.CSharp
TranslatedExpression TranslateWithInitializer(Block block)
{
var stloc = block.Instructions.FirstOrDefault() as StLoc;
var final = block.FinalInstruction as LdLoc;
if (stloc == null || final == null || stloc.Variable != final.Variable || stloc.Variable.Kind != VariableKind.InitializerTarget)
if (block.Instructions.FirstOrDefault() is not StLoc stloc
|| block.FinalInstruction is not LdLoc final
|| stloc.Variable != final.Variable || stloc.Variable.Kind != VariableKind.InitializerTarget)
{
throw new ArgumentException("given Block is invalid!");
}
ILInstruction oldValue = stloc.Value;
if (stloc.Variable.Type.IsReferenceType == true)
{
// reference types must involve a <Clone>$() call
Debug.Assert(TransformCollectionAndObjectInitializers.IsRecordCloneMethodCall((CallInstruction)stloc.Value));
oldValue = ((CallInstruction)stloc.Value).Arguments.Single();
}
WithInitializerExpression withInitializerExpression = new WithInitializerExpression();
withInitializerExpression.Expression = Translate(stloc.Value, stloc.Variable.Type);
withInitializerExpression.Expression = Translate(oldValue, stloc.Variable.Type);
withInitializerExpression.Initializer = BuildArrayInitializerExpression(block, new InitializedObjectResolveResult(stloc.Variable.Type));
return withInitializerExpression.WithILInstruction(block)
@@ -4241,7 +4259,7 @@ namespace ICSharpCode.Decompiler.CSharp
}
else
{
targetType = inst.InferType(compilation);
targetType = PreferNativeIntegerType(inst.InferType(compilation));
}
}
}
@@ -4539,7 +4557,12 @@ namespace ICSharpCode.Decompiler.CSharp
}
}
var value = Translate(inst.Value, typeHint: expectedType);
var operand = inst.Value;
if (operand is LdObjIfRef ldObjIfRef)
{
operand = ldObjIfRef.Target;
}
var value = Translate(operand, typeHint: expectedType);
if (value.Expression is DirectionExpression)
{
// we can deference the managed reference by stripping away the 'ref'
@@ -0,0 +1,457 @@
// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team
//
// 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.
#nullable enable
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.Semantics;
namespace ICSharpCode.Decompiler.CSharp.Transforms
{
/// <summary>
/// Decompiles query expressions.
/// Based on C# 4.0 spec, §7.16.2 Query expression translation
/// </summary>
public class IntroduceQueryExpressions : IAstTransform
{
[AllowNull]
TransformContext context;
public void Run(AstNode rootNode, TransformContext context)
{
if (!context.Settings.QueryExpressions)
return;
this.context = context;
DecompileQueries(rootNode);
// After all queries were decompiled, detect degenerate queries (queries not property terminated with 'select' or 'group')
// and fix them, either by adding a degenerate select, or by combining them with another query.
foreach (QueryExpression query in rootNode.Descendants.OfType<QueryExpression>())
{
QueryFromClause fromClause = (QueryFromClause)query.Clauses.First();
if (IsDegenerateQuery(query))
{
// introduce select for degenerate query
context.Step("Add degenerate query select clause", query);
query.Clauses.Add(new QuerySelectClause { Expression = new IdentifierExpression(fromClause.Identifier).CopyAnnotationsFrom(fromClause) });
}
// See if the data source of this query is a degenerate query,
// and combine the queries if possible.
QueryExpression? innerQuery = fromClause.Expression as QueryExpression;
while (innerQuery != null && IsDegenerateQuery(innerQuery))
{
QueryFromClause innerFromClause = (QueryFromClause)innerQuery.Clauses.First();
ILVariable? innerVariable = innerFromClause.Annotation<ILVariableResolveResult>()?.Variable;
ILVariable? rangeVariable = fromClause.Annotation<ILVariableResolveResult>()?.Variable;
context.Step("Combine nested query clauses", fromClause);
// Replace the fromClause with all clauses from the inner query
fromClause.Remove();
QueryClause? insertionPos = null;
foreach (var clause in innerQuery.Clauses)
{
CombineRangeVariables(clause, innerVariable, rangeVariable);
query.Clauses.InsertAfter(insertionPos, insertionPos = clause.Detach());
}
context.EndStep(innerFromClause);
fromClause = innerFromClause;
innerQuery = fromClause.Expression as QueryExpression;
}
}
}
private void CombineRangeVariables(QueryClause clause, ILVariable? oldVariable, ILVariable? newVariable)
{
if (oldVariable == null || newVariable == null)
return;
foreach (var identifier in clause.DescendantNodes().OfType<Identifier>())
{
var parent = identifier.Parent;
if (parent == null)
continue;
var variable = parent.Annotation<ILVariableResolveResult>()?.Variable;
if (variable == oldVariable)
{
context.Step("Combine query range variables", identifier);
parent.RemoveAnnotations<ILVariableResolveResult>();
parent.AddAnnotation(new ILVariableResolveResult(newVariable));
var newIdentifier = Identifier.Create(newVariable.Name!);
identifier.ReplaceWith(newIdentifier);
context.EndStep(newIdentifier);
}
}
}
bool IsDegenerateQuery(QueryExpression? query)
{
if (query == null)
return false;
var lastClause = query.Clauses.LastOrDefault();
return !(lastClause is QuerySelectClause || lastClause is QueryGroupClause);
}
void DecompileQueries(AstNode node)
{
Expression? query = DecompileQuery(node as InvocationExpression);
if (query != null)
{
if (node.Parent is ExpressionStatement && CanUseDiscardAssignment())
query = new AssignmentExpression(new IdentifierExpression("_"), query);
node.ReplaceWith(query);
context.EndStep(query);
}
AstNode? next;
for (AstNode? child = (query ?? node).FirstChild; child != null; child = next)
{
// store reference to next child before transformation
next = child.NextSibling;
DecompileQueries(child);
}
}
bool CanUseDiscardAssignment()
{
// TODO : check whether there exists a variable named '_' in scope.
return context.Settings.Discards;
}
QueryExpression? DecompileQuery(InvocationExpression? invocation)
{
if (invocation == null)
return null;
MemberReferenceExpression? mre = invocation.Target as MemberReferenceExpression;
if (mre == null || IsNullConditional(mre.Target))
return null;
if (mre.TypeArguments.Count > 0)
return null;
switch (mre.MemberName)
{
case "Select":
{
if (invocation.Arguments.Count != 1)
return null;
if (!IsComplexQuery(mre))
return null;
Expression expr = invocation.Arguments.Single();
if (MatchSimpleLambda(expr, out var parameter, out var body))
{
context.Step("Build select query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach()));
query.Clauses.Add(new QuerySelectClause { Expression = WrapExpressionInParenthesesIfNecessary(body.Detach(), parameter.Name!) }.CopyAnnotationsFrom(expr));
return query;
}
return null;
}
case "GroupBy":
{
if (invocation.Arguments.Count == 2)
{
Expression keyLambda = invocation.Arguments.ElementAt(0);
Expression projectionLambda = invocation.Arguments.ElementAt(1);
if (MatchSimpleLambda(keyLambda, out var parameter1, out var keySelector)
&& MatchSimpleLambda(projectionLambda, out var parameter2, out var elementSelector)
&& parameter1.Name == parameter2.Name)
{
context.Step("Build group query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter1, mre.Target.Detach()));
var queryGroupClause = new QueryGroupClause {
Projection = elementSelector.Detach(),
Key = keySelector.Detach()
};
// The matched group-by lambdas always carry their ILFunction annotation.
queryGroupClause.AddAnnotation(new QueryGroupClauseAnnotation(keyLambda.Annotation<IL.ILFunction>()!, projectionLambda.Annotation<IL.ILFunction>()!));
query.Clauses.Add(queryGroupClause);
return query;
}
}
else if (invocation.Arguments.Count == 1)
{
Expression lambda = invocation.Arguments.Single();
if (MatchSimpleLambda(lambda, out var parameter, out var keySelector))
{
context.Step("Build group query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach()));
query.Clauses.Add(new QueryGroupClause { Projection = new IdentifierExpression(parameter.Name!).CopyAnnotationsFrom(parameter), Key = keySelector.Detach() });
return query;
}
}
return null;
}
case "SelectMany":
{
if (invocation.Arguments.Count != 2)
return null;
var fromExpressionLambda = invocation.Arguments.ElementAt(0);
if (!MatchSimpleLambda(fromExpressionLambda, out var parameter, out var collectionSelector))
return null;
if (IsNullConditional(collectionSelector))
return null;
LambdaExpression? lambda = invocation.Arguments.ElementAt(1) as LambdaExpression;
if (lambda != null && lambda.Parameters.Count == 2 && lambda.Body is Expression)
{
ParameterDeclaration p1 = lambda.Parameters.ElementAt(0);
ParameterDeclaration p2 = lambda.Parameters.ElementAt(1);
if (p1.Name == parameter.Name)
{
context.Step("Build select-many query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(p1, mre.Target.Detach()));
query.Clauses.Add(MakeFromClause(p2, collectionSelector.Detach()).CopyAnnotationsFrom(fromExpressionLambda));
query.Clauses.Add(new QuerySelectClause { Expression = WrapExpressionInParenthesesIfNecessary(((Expression)lambda.Body).Detach(), parameter.Name!) });
return query;
}
}
return null;
}
case "Where":
{
if (invocation.Arguments.Count != 1)
return null;
if (!IsComplexQuery(mre))
return null;
Expression expr = invocation.Arguments.Single();
if (MatchSimpleLambda(expr, out var parameter, out var body))
{
context.Step("Build where query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach()));
query.Clauses.Add(new QueryWhereClause { Condition = body.Detach() }.CopyAnnotationsFrom(expr));
return query;
}
return null;
}
case "OrderBy":
case "OrderByDescending":
case "ThenBy":
case "ThenByDescending":
{
if (invocation.Arguments.Count != 1)
return null;
if (!IsComplexQuery(mre))
return null;
var lambda = invocation.Arguments.Single();
if (MatchSimpleLambda(lambda, out var parameter, out var orderExpression))
{
if (ValidateThenByChain(invocation, parameter.Name!))
{
context.Step("Build order query", invocation);
QueryOrderClause orderClause = new QueryOrderClause();
while (mre.MemberName == "ThenBy" || mre.MemberName == "ThenByDescending")
{
// insert new ordering at beginning
orderClause.Orderings.InsertAfter(
null, new QueryOrdering {
Expression = orderExpression.Detach(),
Direction = (mre.MemberName == "ThenBy" ? QueryOrderingDirection.None : QueryOrderingDirection.Descending)
}.CopyAnnotationsFrom(lambda));
InvocationExpression tmp = (InvocationExpression)mre.Target;
mre = (MemberReferenceExpression)tmp.Target;
lambda = tmp.Arguments.Single();
// ValidateThenByChain already confirmed every clause in the chain is a simple
// lambda, so this match always succeeds.
bool matched = MatchSimpleLambda(lambda, out parameter, out orderExpression);
Debug.Assert(matched);
Debug.Assert(orderExpression != null);
}
// The chain was validated and every iteration re-matched a simple lambda,
// so the final range-variable parameter is set.
Debug.Assert(parameter != null);
// insert new ordering at beginning
orderClause.Orderings.InsertAfter(
null, new QueryOrdering {
Expression = orderExpression.Detach(),
Direction = (mre.MemberName == "OrderBy" ? QueryOrderingDirection.None : QueryOrderingDirection.Descending)
}.CopyAnnotationsFrom(lambda));
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach()));
query.Clauses.Add(orderClause);
return query;
}
}
return null;
}
case "Join":
case "GroupJoin":
{
if (invocation.Arguments.Count != 4)
return null;
Expression source1 = mre.Target;
Expression source2 = invocation.Arguments.ElementAt(0);
if (IsNullConditional(source2))
return null;
Expression outerLambda = invocation.Arguments.ElementAt(1);
if (!MatchSimpleLambda(outerLambda, out var element1, out var key1))
return null;
Expression innerLambda = invocation.Arguments.ElementAt(2);
if (!MatchSimpleLambda(innerLambda, out var element2, out var key2))
return null;
LambdaExpression? lambda = invocation.Arguments.ElementAt(3) as LambdaExpression;
if (lambda != null && lambda.Parameters.Count == 2 && lambda.Body is Expression)
{
ParameterDeclaration p1 = lambda.Parameters.ElementAt(0);
ParameterDeclaration p2 = lambda.Parameters.ElementAt(1);
if (ValidateParameter(p1) && ValidateParameter(p2)
&& p1.Name == element1.Name && (p2.Name == element2.Name || mre.MemberName == "GroupJoin"))
{
context.Step(mre.MemberName == "GroupJoin" ? "Build group join query" : "Build join query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(element1, source1.Detach()));
QueryJoinClause joinClause = new QueryJoinClause();
joinClause.JoinIdentifier = element2.Name!; // join elementName2
joinClause.JoinIdentifierToken.CopyAnnotationsFrom(element2);
joinClause.InExpression = source2.Detach(); // in source2
joinClause.OnExpression = key1.Detach(); // on key1
joinClause.EqualsExpression = key2.Detach(); // equals key2
if (mre.MemberName == "GroupJoin")
{
joinClause.IntoIdentifier = p2.Name; // into p2.Name
joinClause.IntoIdentifierToken!.CopyAnnotationsFrom(p2);
}
// The matched join key-selector lambdas always carry their ILFunction annotation.
joinClause.AddAnnotation(new QueryJoinClauseAnnotation(outerLambda.Annotation<IL.ILFunction>()!, innerLambda.Annotation<IL.ILFunction>()!));
query.Clauses.Add(joinClause);
query.Clauses.Add(new QuerySelectClause { Expression = ((Expression)lambda.Body).Detach() }.CopyAnnotationsFrom(lambda));
return query;
}
}
return null;
}
default:
return null;
}
}
static bool IsComplexQuery(MemberReferenceExpression mre)
{
return (mre.Target is InvocationExpression && mre.Parent is InvocationExpression) || mre.Parent?.Parent is QueryClause;
}
QueryFromClause MakeFromClause(ParameterDeclaration parameter, Expression body)
{
QueryFromClause fromClause = new QueryFromClause {
Identifier = parameter.Name!,
Expression = body
};
fromClause.CopyAnnotationsFrom(parameter);
return fromClause;
}
class ApplyAnnotationVisitor : DepthFirstAstVisitor<AstNode>
{
private LetIdentifierAnnotation annotation;
private string identifier;
public ApplyAnnotationVisitor(LetIdentifierAnnotation annotation, string identifier)
{
this.annotation = annotation;
this.identifier = identifier;
}
public override AstNode VisitIdentifier(Identifier identifier)
{
if (identifier.Name == this.identifier)
identifier.AddAnnotation(annotation);
return identifier;
}
}
internal static bool IsNullConditional(Expression target) => target switch {
UnaryOperatorExpression { Operator: UnaryOperatorType.NullConditional } => true,
MemberReferenceExpression member => IsNullConditional(member.Target),
InvocationExpression invocation => IsNullConditional(invocation.Target),
IndexerExpression { Target: { } indexerTarget } => IsNullConditional(indexerTarget),
_ => false
};
/// <summary>
/// This fixes #437: Decompilation of query expression loses material parentheses
/// We wrap the expression in parentheses if:
/// - the Select-call is explicit (see caller(s))
/// - the expression is a plain identifier matching the parameter name
/// </summary>
Expression WrapExpressionInParenthesesIfNecessary(Expression expression, string parameterName)
{
if (expression is IdentifierExpression ident && parameterName.Equals(ident.Identifier, StringComparison.Ordinal))
return new ParenthesizedExpression(expression);
return expression;
}
/// <summary>
/// Ensure that all ThenBy's are correct, and that the list of ThenBy's is terminated by an 'OrderBy' invocation.
/// </summary>
bool ValidateThenByChain(InvocationExpression? invocation, string expectedParameterName)
{
if (invocation == null || invocation.Arguments.Count != 1)
return false;
if (!(invocation.Target is MemberReferenceExpression mre))
return false;
if (!MatchSimpleLambda(invocation.Arguments.Single(), out var parameter, out _))
return false;
if (parameter.Name != expectedParameterName)
return false;
if (mre.TypeArguments.Count > 0)
return false;
if (mre.MemberName == "OrderBy" || mre.MemberName == "OrderByDescending")
return !IsNullConditional(mre.Target);
else if (mre.MemberName == "ThenBy" || mre.MemberName == "ThenByDescending")
return ValidateThenByChain(mre.Target as InvocationExpression, expectedParameterName);
else
return false;
}
/// <summary>Matches simple lambdas of the form "a => b"</summary>
bool MatchSimpleLambda(Expression expr, [NotNullWhen(true)] out ParameterDeclaration? parameter, [NotNullWhen(true)] out Expression? body)
{
if (expr is LambdaExpression lambda && lambda.Parameters.Count == 1 && lambda.Body is Expression)
{
if (lambda.GetResolveResult() is DecompiledLambdaResolveResult { AttemptedConversionWithTypeMismatch: true })
{
parameter = null;
body = null;
return false;
}
ParameterDeclaration p = lambda.Parameters.Single();
if (ValidateParameter(p))
{
parameter = p;
body = (Expression)lambda.Body;
return true;
}
}
parameter = null;
body = null;
return false;
}
private static bool ValidateParameter(ParameterDeclaration p)
{
return p.ParameterModifier == Decompiler.TypeSystem.ReferenceKind.None && p.Attributes.Count == 0;
}
}
}
File diff suppressed because it is too large. Load diff
@@ -668,12 +668,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
case OpCode.SwitchInstruction:
return true;
case OpCode.LdLoc:
if (v.StateMachineField == null && ((LdLoc)inlinedExpression).Variable.StateMachineField != null)
var copiedVariable = ((LdLoc)inlinedExpression).Variable;
if (v.StateMachineField == null && copiedVariable.StateMachineField != null)
{
// Roslyn likes to put the result of fetching a state machine field into a temporary variable,
// so inline more aggressively in such cases.
return true;
}
if (IsHoistedCompilerTemporary(v) && IsHoistedCompilerTemporary(copiedVariable))
{
return true;
}
break;
}
if (inlinedExpression.ResultType == StackType.Ref)
@@ -773,6 +778,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
}
static bool IsHoistedCompilerTemporary(ILVariable v)
{
string name = v.StateMachineField?.Name;
return name != null
&& (name.StartsWith("<>7__wrap", StringComparison.Ordinal) || name.StartsWith("<>s__", StringComparison.Ordinal));
}
/// <summary>
/// Gets whether 'expressionBeingMoved' can be inlined into 'expr'.
/// </summary>
@@ -942,10 +954,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms
for (int i = 0; i < childIndex; ++i)
{
ILInstruction predecessor = inst.Parent.Children[i];
// The variable of a named argument only exists within its block,
// so only the stored value can interfere with the moved expression.
if (predecessor is StLoc { Variable.Kind: VariableKind.NamedArgument } namedArgument)
predecessor = namedArgument.Value;
if (!IsSafeForInlineOver(predecessor, expressionBeingMoved))
return false;
}
+12
View File
@@ -0,0 +1,12 @@
# Contributions to icsharpcode/ILSpy
.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
This repository holds only my commits to the upstream project, with their original author, date and diff.
Each of my commits is preceded by a snapshot of the upstream files it touched, so every diff shows exactly what I changed.
## Merged pull requests
- [Fix #4137: Keep nint in conditionals with mixed native int branches](https://github.com/icsharpcode/ILSpy/pull/4232) (2026-10-11)
- [Remove the delegate cache left by a discarded method-group conversion](https://github.com/icsharpcode/ILSpy/pull/4133) (2026-09-19)
- [Recognize a VB exception filter's type test](https://github.com/icsharpcode/ILSpy/pull/4134) (2026-09-15)