diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index a70de1e14..e7ab3f26f 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -224,6 +224,14 @@ + + + + + + + + diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index 44d08a2bf..e37502d73 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -435,6 +435,30 @@ namespace ICSharpCode.Decompiler.Tests await Run(); } + [Test] + public async Task ParamsPropertySetter() + { + await Run(); + } + + [Test] + public async Task ParameterizedPropertyInitializer() + { + await Run(); + } + + [Test] + public async Task IndexerAccessorParameterNames() + { + await Run(); + } + + [Test] + public async Task ParameterizedPropertySetterCall() + { + await Run(); + } + async Task Run([CallerMemberName] string testName = null, DecompilerSettings settings = null, AssemblerOptions assemblerOptions = AssemblerOptions.Library) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs index c8107d1ec..6de49b310 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs @@ -32,6 +32,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness Generics(); ConstructorTest(); TestIndexer(); + TestIndexerWithNamedArguments(); + TestRedeclaredDefaultValues(); +#if !MCS2 + TestNamedWithOmittedOptional(); +#endif Issue1281(); Issue1747(); CallAmbiguousOutParam(); @@ -330,6 +335,87 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness } #endregion + #region Named arguments with omitted optional arguments + // mcs 2.6.4 crashes while emitting a call that names its arguments and leaves an optional + // one out. +#if !MCS2 + static void TestNamedWithOmittedOptional() + { + var obj = new NamedOptionalTests(); + obj.M(b: Trace(2), a: Trace(1)); + obj.N(y: Trace(1), x: Trace(2)); + obj.N(z: Trace(1), x: Trace(2)); + } + + class NamedOptionalTests + { + public void M(int a, int b, int c = 3) + { + Console.WriteLine("M(" + a + ", " + b + ", " + c + ")"); + } + + public void N(int x, int y = 10, int z = 20) + { + Console.WriteLine("N(" + x + ", " + y + ", " + z + ")"); + } + } +#endif + #endregion + + #region Redeclared default values + static void TestRedeclaredDefaultValues() + { + var derived = new DerivedDefaultValue(); + Console.WriteLine(derived[1, 10]); + Console.WriteLine(derived.Method(1, 10)); + } + + class BaseDefaultValue + { + public virtual int this[int x, int y = 10] { + get { + return x + y; + } + } + + public virtual int Method(int x, int y = 10) + { + return x + y; + } + } + + class DerivedDefaultValue : BaseDefaultValue + { + public override int this[int x, int y = 20] { + get { + return x + y + 1; + } + } + + public override int Method(int x, int y = 20) + { + return x + y + 1; + } + } + #endregion + + #region Indexer with named arguments + static void TestIndexerWithNamedArguments() + { + var obj = new NamedArgumentIndexerTests(); + Console.WriteLine(obj[y: Trace(1), x: Trace(2)]); + obj[y: Trace(3), x: Trace(4)] = Trace(5); + Console.WriteLine(obj[y: Trace(6), x: Trace(7)] = Trace(8)); + obj[y: Trace(9), x: Trace(10)] += 5; + } + + static int Trace(int i) + { + Console.WriteLine("Trace(" + i + ")"); + return i; + } + #endregion + #region Out Parameter static void AmbiguousOutParam(out string a) { @@ -602,6 +688,19 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness #endregion } + class NamedArgumentIndexerTests + { + public int this[int x, int y] { + get { + Console.WriteLine("get_Item(" + x + ", " + y + ")"); + return x; + } + set { + Console.WriteLine("set_Item(" + x + ", " + y + ", " + value + ")"); + } + } + } + class IndexerTests { public object this[object key] { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/IndexerAccessorParameterNames.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/IndexerAccessorParameterNames.cs new file mode 100644 index 000000000..ae421c420 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/IndexerAccessorParameterNames.cs @@ -0,0 +1,20 @@ +public class IndexerAccessorParameterNames +{ + public int this[int x, int y] { + get { + return x; + } + set { + } + } + + private int Get(int i) + { + return i; + } + + public void Use() + { + this[y: Get(1), x: Get(2)] = 3; + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/IndexerAccessorParameterNames.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/IndexerAccessorParameterNames.il new file mode 100644 index 000000000..6a4ad53ba --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/IndexerAccessorParameterNames.il @@ -0,0 +1,70 @@ +#define CORE_ASSEMBLY "System.Runtime" + +.assembly extern CORE_ASSEMBLY +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....: + .ver 4:0:0:0 +} + +.assembly IndexerAccessorParameterNames { } + +.class public auto ansi beforefieldinit IndexerAccessorParameterNames + extends [CORE_ASSEMBLY]System.Object +{ + .custom instance void [CORE_ASSEMBLY]System.Reflection.DefaultMemberAttribute::.ctor(string) = ( 01 00 04 49 74 65 6D 00 00 ) + + // The indexer's parameter names are the getter's: x and y. + .method public hidebysig specialname instance int32 get_Item (int32 x, int32 y) cil managed + { + .maxstack 8 + ldarg.1 + ret + } + + // The setter is free to name the same parameters differently, which C# cannot express. + .method public hidebysig specialname instance void set_Item (int32 a, int32 b, int32 'value') cil managed + { + .maxstack 8 + ret + } + + .property instance int32 Item(int32, int32) + { + .get instance int32 IndexerAccessorParameterNames::get_Item(int32, int32) + .set instance void IndexerAccessorParameterNames::set_Item(int32, int32, int32) + } + + .method private hidebysig instance int32 Get (int32 i) cil managed + { + .maxstack 8 + ldarg.1 + ret + } + + // this[y: Get(1), x: Get(2)] = 3; + .method public hidebysig instance void Use () cil managed + { + .maxstack 4 + .locals init (int32 V_0) + ldarg.0 + ldarg.0 + ldc.i4.1 + call instance int32 IndexerAccessorParameterNames::Get(int32) + stloc.0 + ldarg.0 + ldc.i4.2 + call instance int32 IndexerAccessorParameterNames::Get(int32) + ldloc.0 + ldc.i4.3 + call instance void IndexerAccessorParameterNames::set_Item(int32, int32, int32) + ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor () cil managed + { + .maxstack 8 + ldarg.0 + call instance void [CORE_ASSEMBLY]System.Object::.ctor() + ret + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertyInitializer.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertyInitializer.cs new file mode 100644 index 000000000..0b1347b34 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertyInitializer.cs @@ -0,0 +1,21 @@ +public class ParameterizedPropertyInitializer +{ + // C# has no syntax for parameterized property 'Foo'. + public int get_Foo(int x) + { + return x; + } + + public void set_Foo(int x, int value) + { + } + + public static void Consume(ParameterizedPropertyInitializer p) + { + } + + public static void Use() + { + Consume(new ParameterizedPropertyInitializer { [7] = 5 }); + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertyInitializer.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertyInitializer.il new file mode 100644 index 000000000..38a835abd --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertyInitializer.il @@ -0,0 +1,60 @@ +#define CORE_ASSEMBLY "System.Runtime" + +.assembly extern CORE_ASSEMBLY +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....: + .ver 4:0:0:0 +} + +.assembly ParameterizedPropertyInitializer { } + +// A parameterized property that is not an indexer: the type carries no DefaultMemberAttribute, +// which C# cannot express, but VB, C++/CLI and COM interop all produce it. +.class public auto ansi beforefieldinit ParameterizedPropertyInitializer + extends [CORE_ASSEMBLY]System.Object +{ + .method public hidebysig specialname instance int32 get_Foo (int32 x) cil managed + { + .maxstack 8 + ldarg.1 + ret + } + + .method public hidebysig specialname instance void set_Foo (int32 x, int32 'value') cil managed + { + .maxstack 8 + ret + } + + .property instance int32 Foo(int32) + { + .get instance int32 ParameterizedPropertyInitializer::get_Foo(int32) + .set instance void ParameterizedPropertyInitializer::set_Foo(int32, int32) + } + + .method public hidebysig static void Consume (class ParameterizedPropertyInitializer p) cil managed + { + .maxstack 8 + ret + } + + .method public hidebysig static void Use () cil managed + { + .maxstack 8 + newobj instance void ParameterizedPropertyInitializer::.ctor() + dup + ldc.i4.7 + ldc.i4.5 + call instance void ParameterizedPropertyInitializer::set_Foo(int32, int32) + call void ParameterizedPropertyInitializer::Consume(class ParameterizedPropertyInitializer) + ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor () cil managed + { + .maxstack 8 + ldarg.0 + call instance void [CORE_ASSEMBLY]System.Object::.ctor() + ret + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertySetterCall.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertySetterCall.cs new file mode 100644 index 000000000..8167f578f --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertySetterCall.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +public class ParameterizedPropertySetterCall +{ + // C# has no syntax for parameterized property 'P'. + public int get_P(int i) + { + return i; + } + + public void set_P([Optional][DefaultParameterValue(0)] int i, int value) + { + } + + public void Use() + { + this.set_P(0, 5); + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertySetterCall.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertySetterCall.il new file mode 100644 index 000000000..0582a2dea --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParameterizedPropertySetterCall.il @@ -0,0 +1,55 @@ +#define CORE_ASSEMBLY "System.Runtime" + +.assembly extern CORE_ASSEMBLY +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....: + .ver 4:0:0:0 +} + +.assembly ParameterizedPropertySetterCall { } + +// A parameterized property whose setter takes an index and the assigned value. It is not the +// type's default member, so there is no access syntax for it and the accessor is written as a +// call - which means the assigned value is an ordinary argument, and the optional index before +// it is not trailing. +.class public auto ansi beforefieldinit ParameterizedPropertySetterCall + extends [CORE_ASSEMBLY]System.Object +{ + .method public hidebysig specialname instance int32 get_P (int32 i) cil managed + { + .maxstack 8 + ldarg.1 + ret + } + + .method public hidebysig specialname instance void set_P ([opt] int32 i, int32 'value') cil managed + { + .param [1] = int32(0x00000000) + .maxstack 8 + ret + } + + .property instance int32 P(int32) + { + .get instance int32 ParameterizedPropertySetterCall::get_P(int32) + .set instance void ParameterizedPropertySetterCall::set_P(int32, int32) + } + + .method public hidebysig instance void Use () cil managed + { + .maxstack 8 + ldarg.0 + ldc.i4.0 + ldc.i4.5 + call instance void ParameterizedPropertySetterCall::set_P(int32, int32) + ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor () cil managed + { + .maxstack 8 + ldarg.0 + call instance void [CORE_ASSEMBLY]System.Object::.ctor() + ret + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParamsPropertySetter.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParamsPropertySetter.cs new file mode 100644 index 000000000..7149658d3 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParamsPropertySetter.cs @@ -0,0 +1,18 @@ +public class ParamsPropertySetter +{ + private int[] values; + + public int[] Values { + get { + return values; + } + set { + values = value; + } + } + + public void Use() + { + Values = new int[2] { 1, 2 }; + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParamsPropertySetter.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParamsPropertySetter.il new file mode 100644 index 000000000..10fbb2bff --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/ParamsPropertySetter.il @@ -0,0 +1,68 @@ +#define CORE_ASSEMBLY "System.Runtime" + +.assembly extern CORE_ASSEMBLY +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....: + .ver 4:0:0:0 +} + +.assembly ParamsPropertySetter { } + +.class public auto ansi beforefieldinit ParamsPropertySetter + extends [CORE_ASSEMBLY]System.Object +{ + .field private int32[] 'values' + + .method public hidebysig specialname instance int32[] get_Values () cil managed + { + .maxstack 8 + ldarg.0 + ldfld int32[] ParamsPropertySetter::'values' + ret + } + + // C# cannot declare a property whose value is a parameter array, and an assignment has no + // argument list to expand one into. + .method public hidebysig specialname instance void set_Values (int32[] 'value') cil managed + { + .param [1] + .custom instance void [CORE_ASSEMBLY]System.ParamArrayAttribute::.ctor() = ( 01 00 00 00 ) + .maxstack 8 + ldarg.0 + ldarg.1 + stfld int32[] ParamsPropertySetter::'values' + ret + } + + .property instance int32[] Values() + { + .get instance int32[] ParamsPropertySetter::get_Values() + .set instance void ParamsPropertySetter::set_Values(int32[]) + } + + .method public hidebysig instance void Use () cil managed + { + .maxstack 4 + ldarg.0 + ldc.i4.2 + newarr [CORE_ASSEMBLY]System.Int32 + dup + ldc.i4.0 + ldc.i4.1 + stelem.i4 + dup + ldc.i4.1 + ldc.i4.2 + stelem.i4 + call instance void ParamsPropertySetter::set_Values(int32[]) + ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor () cil managed + { + .maxstack 8 + ldarg.0 + call instance void [CORE_ASSEMBLY]System.Object::.ctor() + ret + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NamedArguments.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NamedArguments.cs index 293d23e04..77d84517e 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NamedArguments.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NamedArguments.cs @@ -60,6 +60,68 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } } + public class BaseNames + { + public virtual int this[int x, int y] { + get { + return x; + } + set { + } + } + } + + public class DerivedNames : BaseNames + { + public override int this[int a, int b] { + get { + return a; + } + set { + } + } + } + + public int this[int x, int y] { + get { + return x; + } + set { + } + } + + public int this[int i, object o] { + get { + return i; + } + set { + } + } + + public int this[int i, string o] { + get { + return i; + } + set { + } + } + + public int this[int a, int b, int c = 30] { + get { + return a; + } + set { + } + } + + public void UseOptional(int a, int b, int c = 3) + { + } + + public void UseTwoOptional(int x, int y = 10, int z = 20) + { + } + public void Use(int a, int b, int c) { } @@ -81,5 +143,34 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty int b = Get(1); Use(Get(2), b, Get(3)); } + + public void NamedArgsForIndexer() + { + Use(this[y: Get(1), x: Get(2)], 0, 0); + this[y: Get(1), x: Get(2)] = 3; + } + + public void NamedArgsWithOmittedOptional() + { + UseOptional(b: Get(2), a: Get(1)); + UseTwoOptional(y: Get(1), x: Get(2)); + Use(this[b: Get(1), a: Get(2)], 0, 0); + this[b: Get(1), a: Get(2)] = 4; + } + + public void NamedArgsWithOmittedMiddleOptional() + { + UseTwoOptional(z: Get(1), x: Get(2)); + } + + public void NamedArgsForIndexerNeedingCast() + { + Use(this[o: (object)((Get(1) == 1) ? "a" : "b"), i: Get(2)], 0, 0); + } + public void NamedArgsForOverriddenIndexer(DerivedNames derived) + { + // The names are the base indexer's, which is what the call instruction names. + Use(((BaseNames)derived)[y: Get(1), x: Get(2)], 0, 0); + } } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs index b45e349b7..0be76eb59 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs @@ -55,6 +55,115 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } } + internal class Indexer + { + public int this[int x, int y = 10] { + get { + return x + y; + } + set { + } + } + } + + internal class IndexerWithOverload + { + public int this[int x] { + get { + return x; + } + set { + } + } + + public int this[int x, int y = 10] { + get { + return x + y; + } + set { + } + } + } + + internal class AllOptionalIndexer + { + public int this[int x = 10, int y = 20] { + get { + return x + y; + } + set { + } + } + } + + internal class BaseDefaultValue + { + public virtual int this[int x, int y = 10] { + get { + return x + y; + } + set { + } + } + + public virtual int Method(int x, int y = 10) + { + return x + y; + } + } + + internal class DerivedDefaultValue : BaseDefaultValue + { + public override int this[int x, int y = 20] { + get { + return x + y + 1; + } + set { + } + } + + public override int Method(int x, int y = 20) + { + return x + y + 1; + } + } + + internal class BaseIndexer + { + public virtual int this[bool flag] { + get { + return 1; + } + set { + } + } + } + + internal class DerivedIndexer : BaseIndexer + { + public override int this[bool f] { + get { + return 2; + } + set { + } + } + } + + [StructLayout(LayoutKind.Sequential, Size = 1)] + internal struct StructIndexer + { + public int this[int x, int y = 10] { + get { + return x + y; + } + set { + } + } + } + + private static StructIndexer structIndexer; + public OptionalArguments(string name, int a = 5) { @@ -330,5 +439,62 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty d(42); } #endif + + private void RedeclaredDefaultValues(DerivedDefaultValue derived) + { + // The calls go to the base declarations, whose defaults the override redeclares: + // leaving the argument out would pass the override's value instead. + Console.WriteLine(derived[1, 10]); + derived[1, 10] = 5; + Console.WriteLine(derived.Method(1, 10)); + } + + private void Indexers(Indexer indexer, IndexerWithOverload overloaded) + { + Console.WriteLine(indexer[1]); + Console.WriteLine(indexer[1, 20]); + indexer[1] = 5; + indexer[1] += 5; + indexer[1]++; + Console.WriteLine(structIndexer[1]); + structIndexer[1] = 5; + // Leaving the argument out would bind to the single-parameter indexer. + Console.WriteLine(overloaded[1, 10]); + Console.WriteLine(overloaded[1]); + } + + private void AllOptionalIndexers(AllOptionalIndexer allOptional, BaseIndexer boolIndexer, DerivedIndexer derived) + { + // An indexer access keeps an argument even when every one of them is optional. + Console.WriteLine(allOptional[10]); + allOptional[10] = 5; + // Primitive values are not named in an access, unlike in a call. The second one binds + // to an override that names the parameter differently. + Console.WriteLine(boolIndexer[true]); + Console.WriteLine(derived[true]); + } + + // Only the index initializers below need C# 6. +#if CS60 + private Indexer IndexerInitializer() + { + return new Indexer { + [1] = 5, + [2, 20] = 6 + }; + } + + private BaseIndexer BoolIndexerInitializer() + { + // An index initializer does not name its arguments either. + return new BaseIndexer { [true] = 5 }; + } + + private DerivedIndexer DerivedIndexerInitializer() + { + // The call goes to the base indexer, which names the parameter differently. + return new DerivedIndexer { [true] = 7 }; + } +#endif } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArgumentsDisabled.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArgumentsDisabled.cs index b33456f2c..5a87ea695 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArgumentsDisabled.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArgumentsDisabled.cs @@ -1,7 +1,17 @@ +using System; + namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { public class OptionalArgumentsDisabled { + public int this[int x, int y = 10] { + get { + return x + y; + } + set { + } + } + public void Test() { MixedArguments("123", 0, 0); @@ -15,5 +25,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public void OnlyOptionalArguments(int a = 0, int b = 0) { } + + public void TestIndexer() + { + Console.WriteLine(this[1, 10]); + this[1, 10] = 5; + } } } diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 1836d8b7f..2e80834e9 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -58,12 +58,15 @@ namespace ICSharpCode.Decompiler.CSharp public bool AddNamesToPrimitiveValues; public bool UseImplicitlyTypedOut; public bool IsExpandedForm; + public bool IsSetter; public int Length => Arguments.Length; - private int GetActualArgumentCount() + public int GetActualArgumentCount() { + int count = IsSetter ? Arguments.Length - 1 : Arguments.Length; if (FirstOptionalArgumentIndex < 0) - return Arguments.Length; + return count; + Debug.Assert(FirstOptionalArgumentIndex <= count); return FirstOptionalArgumentIndex; } @@ -74,10 +77,11 @@ namespace ICSharpCode.Decompiler.CSharp && !ParameterNames.Any(string.IsNullOrEmpty)) { Debug.Assert(skipCount == 0); - if (argumentNames == null) - { - argumentNames = new string[Arguments.Length]; - } + // On a copy: giving these names up again must leave the ones that order the + // arguments untouched. + argumentNames = argumentNames == null + ? new string[Arguments.Length] + : (string[])argumentNames.Clone(); for (int i = 0; i < Arguments.Length; i++) { @@ -88,10 +92,19 @@ namespace ICSharpCode.Decompiler.CSharp } } + // The names cover the full parameter list and have to stop where the arguments do. + int argumentCount = GetActualArgumentCount(); + if (argumentNames != null && argumentNames.Length > argumentCount) + { + var writtenNames = new string[argumentCount]; + Array.Copy(argumentNames, writtenNames, argumentCount); + argumentNames = writtenNames; + } + return argumentNames; } - public IList GetArgumentResolveResults(int skipCount = 0) + public ResolveResult[] GetArgumentResolveResults(int skipCount = 0) { var expectedParameters = ExpectedParameters; var useImplicitlyTypedOut = UseImplicitlyTypedOut; @@ -111,7 +124,7 @@ namespace ICSharpCode.Decompiler.CSharp } } - public IList GetArgumentResolveResultsDirect(int skipCount = 0) + public ResolveResult[] GetArgumentResolveResultsDirect(int skipCount = 0) { return Arguments .Skip(skipCount) @@ -132,7 +145,7 @@ namespace ICSharpCode.Decompiler.CSharp else { Debug.Assert(skipCount == 0); - return Arguments.Take(argumentCount).Zip(argumentNames.Take(argumentCount), + return Arguments.Take(argumentCount).Zip(argumentNames, (arg, name) => { if (name == null) return AddAnnotations(arg.Expression); @@ -500,11 +513,17 @@ namespace ICSharpCode.Decompiler.CSharp return result; } - int allowedParamCount = (method.ReturnType.IsKnownType(KnownTypeCode.Void) ? 1 : 0); - if (method.IsAccessor && (method.AccessorOwner.SymbolKind == SymbolKind.Indexer || argumentList.ExpectedParameters.Length == allowedParamCount)) + // IsSetter carries the answer for an accessor that takes the assigned value, including + // the argument order that keeps it last. + if (argumentList.IsSetter || (!TakesAssignedValueLast(method) && IsWrittenAsMemberAccess(method))) { - argumentList.CheckNoNamedOrOptionalArguments(); - return HandleAccessorCall(expectedTargetDetails, method, target, argumentList.Arguments.ToList(), argumentList.ArgumentNames); + // Only an indexer access has an argument list to carry names or leave arguments out of. + if (method.AccessorOwner!.SymbolKind != SymbolKind.Indexer) + argumentList.CheckNoNamedOrOptionalArguments(); + // An access spells its index out anyway, and the ladder answers a name the member + // does not have with a cast of the target rather than by giving the name up. + argumentList.AddNamesToPrimitiveValues = false; + return HandleAccessorCall(expectedTargetDetails, method, target, argumentList); } if (IsDelegateEqualityComparison(method, argumentList.Arguments)) @@ -793,10 +812,15 @@ namespace ICSharpCode.Decompiler.CSharp callArguments.Add(value ?? new Nop()); var argumentList = BuildArgumentList(expectedTargetDetails, target, method, 1, callArguments, null); + // An index initializer is an assignment whatever the accessor looks like, even for a + // parameterized property, which has no access syntax of its own. + argumentList.IsSetter = true; + // The cast the ladder would answer an unresolvable name with is removed again below, + // together with the target. + argumentList.AddNamesToPrimitiveValues = false; var unused = new IdentifierExpression("initializedObject").WithRR(target).WithoutILInstruction(); - var assignment = HandleAccessorCall(expectedTargetDetails, method, unused, - argumentList.Arguments.ToList(), argumentList.ArgumentNames); + var assignment = HandleAccessorCall(expectedTargetDetails, method, unused, argumentList); if (((AssignmentExpression)assignment).Left is IndexerExpression indexer && indexer.Target is not null) indexer.Target.Remove(); @@ -1013,6 +1037,22 @@ namespace ICSharpCode.Decompiler.CSharp // >= 0 - the index of the first argument that can be removed, because it is optional // and is the default value of the parameter. int firstOptionalArgumentIndex = expressionBuilder.settings.OptionalArguments ? -2 : -1; + // Only an access takes the assigned value out of the argument list; an accessor + // written as a call passes it like any other argument. + bool writtenAsMemberAccess = IsWrittenAsMemberAccess(method); + if (writtenAsMemberAccess && TakesAssignedValueLast(method) && argumentToParameterMap != null + && argumentToParameterMap[callArguments.Count - 1] != method.Parameters.Count - 1) + { + // An access writes the value from the last argument; in any other order there is + // no access syntax for it. + writtenAsMemberAccess = false; + } + bool isSetter = writtenAsMemberAccess && TakesAssignedValueLast(method); + // A name in an element access names a parameter of the indexer, which the type system + // takes from the getter; the accessor being called may name them differently. + IReadOnlyList namedParameters = method.AccessorOwner is IProperty { IsIndexer: true } indexer + ? indexer.Parameters + : method.Parameters; for (int i = firstParamIndex; i < callArguments.Count; i++) { IParameter parameter; @@ -1024,10 +1064,13 @@ namespace ICSharpCode.Decompiler.CSharp // assign names to that argument and all following arguments: argumentNames = new string[method.Parameters.Count]; } - parameter = method.Parameters[argumentToParameterMap[i]]; - if (argumentNames != null && AssignVariableNames.IsValidName(parameter.Name)) + int parameterIndex = argumentToParameterMap[i]; + parameter = method.Parameters[parameterIndex]; + // The assigned value is past the end of the indexer's parameters. + if (argumentNames != null && parameterIndex < namedParameters.Count + && AssignVariableNames.IsValidName(namedParameters[parameterIndex].Name)) { - argumentNames[arguments.Count] = parameter.Name; + argumentNames[arguments.Count] = namedParameters[parameterIndex].Name; } } else @@ -1039,17 +1082,24 @@ namespace ICSharpCode.Decompiler.CSharp { isPrimitiveValue.Set(arguments.Count); } - if (IsOptionalArgument(parameter, arg)) + // The assigned value of a setter is not part of the argument list, so it does not + // end the run of optional arguments either. + if (!(isSetter && i + 1 == callArguments.Count)) { - if (firstOptionalArgumentIndex == -2) - firstOptionalArgumentIndex = i - firstParamIndex; - } - else - { - if (firstOptionalArgumentIndex != -1) + if (IsOptionalArgument(parameter, arg)) + { + if (firstOptionalArgumentIndex == -2) + firstOptionalArgumentIndex = i - firstParamIndex; + } + else if (firstOptionalArgumentIndex != -1) + { firstOptionalArgumentIndex = -2; + } } - if (expressionBuilder.settings.ExpandParamsArguments && parameter.IsParams && i + 1 == callArguments.Count && argumentToParameterMap == null) + // An assignment has no argument list to spread a parameter array over, and C# + // cannot declare a property whose value is one. + if (expressionBuilder.settings.ExpandParamsArguments && parameter.IsParams && !isSetter + && i + 1 == callArguments.Count && argumentToParameterMap == null) { // Parameter is marked params // If the argument is an array creation, inline all elements into the call and add missing default values. @@ -1111,6 +1161,7 @@ namespace ICSharpCode.Decompiler.CSharp list.IsExpandedForm = isExpandedForm; list.IsPrimitiveValue = isPrimitiveValue; list.FirstOptionalArgumentIndex = firstOptionalArgumentIndex; + list.IsSetter = isSetter; list.UseImplicitlyTypedOut = true; list.AddNamesToPrimitiveValues = expressionBuilder.settings.NamedArguments && expressionBuilder.settings.NonTrailingNamedArguments; return list; @@ -1133,8 +1184,7 @@ namespace ICSharpCode.Decompiler.CSharp expandedParameters.InsertRange(0, expectedParameters); expandedArguments.InsertRange(0, arguments); if (IsUnambiguousCall(expectedTargetDetails, method, targetResolveResult, Empty.Array, - expandedArguments.SelectArray(a => a.ResolveResult), argumentNames: null, - firstOptionalArgumentIndex: -1, out _, + expandedArguments.SelectArray(a => a.ResolveResult), argumentNames: null, out _, out var bestCandidateIsExpandedForm) == OverloadResolutionErrors.None && bestCandidateIsExpandedForm) { expectedParameters = expandedParameters; @@ -1214,6 +1264,35 @@ namespace ICSharpCode.Decompiler.CSharp } } + /// + /// Whether the omitted arguments are the defaults of the member the shortened call resolves + /// to. They were compared against the method the call instruction names, which for a virtual + /// call is the base declaration, and an override may redeclare a different default. + /// + bool OmittedArgumentsAreDefaultsOf(ArgumentList argumentList, IMember? foundMember) + { + int argumentCount = argumentList.IsSetter ? argumentList.Length - 1 : argumentList.Length; + int omittedFrom = argumentList.GetActualArgumentCount(); + if (omittedFrom >= argumentCount) + return true; + if (foundMember is not IParameterizedMember foundParameterizedMember) + return false; + var parameters = foundParameterizedMember.Parameters; + // Names may leave out a parameter in the middle, so what was dropped is found through + // the map rather than by position. Its first entries are the target's. + var map = argumentList.ArgumentToParameterMap; + int firstParamIndex = map != null ? map.Count - argumentList.Length : 0; + for (int i = omittedFrom; i < argumentCount; i++) + { + int parameterIndex = map != null ? map[i + firstParamIndex] : i; + if (parameterIndex < 0 || parameterIndex >= parameters.Count) + return false; + if (!IsOptionalArgument(parameters[parameterIndex], argumentList.Arguments[i])) + return false; + } + return true; + } + bool IsOptionalArgument(IParameter parameter, TranslatedExpression arg) { if (!parameter.IsOptional) @@ -1245,13 +1324,46 @@ namespace ICSharpCode.Decompiler.CSharp private CallTransformation GetRequiredTransformationsForCall(ExpectedTargetDetails expectedTargetDetails, IMethod method, ref TranslatedExpression target, ref ArgumentList argumentList, CallTransformation allowedTransforms, out IParameterizedMember? foundMethod) + { + var transform = GetRequiredTransformations(expectedTargetDetails, method, ref target, ref argumentList, + allowedTransforms, writtenAsMemberAccess: false, out var foundMember); + foundMethod = (IParameterizedMember?)foundMember; + return transform; + } + + /// + /// Finds the transformations an expression needs to bind back to the member the IL names, + /// cheapest first. With the expression is a + /// property, indexer or event access, which resolves against the accessor's owner. + /// + private CallTransformation GetRequiredTransformations(ExpectedTargetDetails expectedTargetDetails, IMethod method, + ref TranslatedExpression target, ref ArgumentList argumentList, CallTransformation allowedTransforms, + bool writtenAsMemberAccess, out IMember? foundMember) { CallTransformation transform = CallTransformation.None; + IMember boundMember = writtenAsMemberAccess ? method.AccessorOwner! : method; // initialize requireTarget flag bool requireTarget; ResolveResult? targetResolveResult; - if ((allowedTransforms & CallTransformation.RequireTarget) != 0) + if (writtenAsMemberAccess) + { + if (settings.AlwaysQualifyMemberReferences || boundMember.SymbolKind == SymbolKind.Indexer + || expressionBuilder.HidesVariableWithName(boundMember.Name)) + { + requireTarget = true; + } + else if (method.IsStatic) + { + requireTarget = !expressionBuilder.IsCurrentOrContainingType(method.DeclaringTypeDefinition); + } + else + { + requireTarget = target.Expression is not ThisReferenceExpression; + } + targetResolveResult = requireTarget ? target.ResolveResult : null; + } + else if ((allowedTransforms & CallTransformation.RequireTarget) != 0) { if (settings.AlwaysQualifyMemberReferences || expressionBuilder.HidesVariableWithName(method.Name)) { @@ -1325,14 +1437,38 @@ namespace ICSharpCode.Decompiler.CSharp } bool targetCasted = false; - bool argumentsCasted = false; + bool argumentsCasted = writtenAsMemberAccess && argumentList.GetActualArgumentCount() == 0; bool originalRequireTarget = requireTarget; - bool skipTargetCast = method.Accessibility <= Accessibility.Protected && expressionBuilder.IsBaseTypeOfCurrentType(method.DeclaringTypeDefinition); + bool skipTargetCast = !writtenAsMemberAccess + && method.Accessibility <= Accessibility.Protected && expressionBuilder.IsBaseTypeOfCurrentType(method.DeclaringTypeDefinition); OverloadResolutionErrors errors; - while ((errors = IsUnambiguousCall(expectedTargetDetails, method, targetResolveResult, typeArguments, - argumentList.GetArgumentResolveResults().ToArray(), argumentList.GetArgumentNames(), argumentList.FirstOptionalArgumentIndex, out foundMethod, - out var bestCandidateIsExpandedForm)) != OverloadResolutionErrors.None || bestCandidateIsExpandedForm != argumentList.IsExpandedForm) + while (true) { + bool expandedFormMismatch = false; + if (writtenAsMemberAccess) + { + errors = IsUnambiguousAccess(expectedTargetDetails, targetResolveResult, method, + argumentList.GetArgumentResolveResults(), argumentList.GetArgumentNames(), out foundMember); + } + else + { + errors = IsUnambiguousCall(expectedTargetDetails, method, targetResolveResult, typeArguments, + argumentList.GetArgumentResolveResults(), argumentList.GetArgumentNames(), + out var foundMethod, out bool bestCandidateIsExpandedForm); + foundMember = foundMethod; + expandedFormMismatch = bestCandidateIsExpandedForm != argumentList.IsExpandedForm; + } + if (errors == OverloadResolutionErrors.None && !expandedFormMismatch + && OmittedArgumentsAreDefaultsOf(argumentList, foundMember)) + { + break; + } + if (errors == OverloadResolutionErrors.None && argumentList.FirstOptionalArgumentIndex >= 0) + { + // The omitted arguments are not the defaults of the member found. + argumentList.FirstOptionalArgumentIndex = -1; + continue; + } switch (errors) { case OverloadResolutionErrors.OutVarTypeMismatch: @@ -1380,7 +1516,8 @@ namespace ICSharpCode.Decompiler.CSharp } argumentsCasted = true; argumentList.UseImplicitlyTypedOut = false; - CastArguments(argumentList.Arguments, argumentList.ExpectedParameters); + CastArguments(argumentList.Arguments, argumentList.GetActualArgumentCount(), + argumentList.ExpectedParameters); } else if ((allowedTransforms & CallTransformation.RequireTarget) != 0 && !requireTarget) { @@ -1399,7 +1536,7 @@ namespace ICSharpCode.Decompiler.CSharp else { targetCasted = true; - target = target.ConvertTo(method.DeclaringType, expressionBuilder); + target = target.ConvertTo(boundMember.DeclaringType, expressionBuilder); targetResolveResult = target.ResolveResult; } } @@ -1420,7 +1557,7 @@ namespace ICSharpCode.Decompiler.CSharp continue; } // We've given up. - foundMethod = method; + foundMember = boundMember; break; } if ((allowedTransforms & CallTransformation.RequireTarget) != 0 && requireTarget) @@ -1537,9 +1674,13 @@ namespace ICSharpCode.Decompiler.CSharp return newObj; } - private void CastArguments(IList arguments, IList expectedParameters) + /// + /// Casts the first arguments in place - the retry loop reads them + /// back from the array on its next attempt. A setter's assigned value is past the count. + /// + private void CastArguments(TranslatedExpression[] arguments, int count, IParameter[] expectedParameters) { - for (int i = 0; i < arguments.Count; i++) + for (int i = 0; i < count; i++) { if (settings.AnonymousTypes && expectedParameters[i].Type.ContainsAnonymousType()) { @@ -1655,7 +1796,7 @@ namespace ICSharpCode.Decompiler.CSharp OverloadResolutionErrors IsUnambiguousCall(ExpectedTargetDetails expectedTargetDetails, IMethod method, ResolveResult? target, IType[] typeArguments, ResolveResult[] arguments, - string[]? argumentNames, int firstOptionalArgumentIndex, + string[]? argumentNames, out IParameterizedMember? foundMember, out bool bestCandidateIsExpandedForm) { foundMember = null; @@ -1666,10 +1807,6 @@ namespace ICSharpCode.Decompiler.CSharp Log.WriteLine("IsUnambiguousCall: Performing overload resolution for " + method); Log.WriteCollection(" Arguments: ", arguments); - argumentNames = firstOptionalArgumentIndex < 0 || argumentNames == null - ? argumentNames - : argumentNames.Take(firstOptionalArgumentIndex).ToArray(); - var or = new OverloadResolution(resolver.Compilation, arguments, argumentNames, typeArguments, conversions: expressionBuilder.resolver.conversions); @@ -1771,11 +1908,15 @@ namespace ICSharpCode.Decompiler.CSharp return OverloadResolutionErrors.None; } - bool IsUnambiguousAccess(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method, - IList arguments, string[]? argumentNames, [NotNullWhen(true)] out IMember? foundMember) + /// + /// Resolves an access the way a call is resolved, so that the ladder can tell one that binds + /// to nothing from one that is merely missing an argument. + /// + OverloadResolutionErrors IsUnambiguousAccess(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method, + ResolveResult[] arguments, string[]? argumentNames, out IMember? foundMember) { Log.WriteLine("IsUnambiguousAccess: Performing overload resolution for " + method); - Log.WriteCollection(" Arguments: ", arguments.Select(a => a.ResolveResult)); + Log.WriteCollection(" Arguments: ", arguments); foundMember = null; if (target == null) @@ -1784,7 +1925,7 @@ namespace ICSharpCode.Decompiler.CSharp EmptyList.Instance, isInvocationTarget: false) as MemberResolveResult; if (result == null || result.IsError) - return false; + return OverloadResolutionErrors.AmbiguousMatch; foundMember = result.Member; } else @@ -1793,15 +1934,15 @@ namespace ICSharpCode.Decompiler.CSharp if (method.AccessorOwner!.SymbolKind == SymbolKind.Indexer) { var or = new OverloadResolution(resolver.Compilation, - arguments.SelectArray(a => a.ResolveResult), + arguments, argumentNames: argumentNames, typeArguments: Empty.Array, conversions: expressionBuilder.resolver.conversions); or.AddMethodLists(lookup.LookupIndexers(target)); if (or.BestCandidateErrors != OverloadResolutionErrors.None) - return false; + return or.BestCandidateErrors; if (or.IsAmbiguous) - return false; + return OverloadResolutionErrors.AmbiguousMatch; foundMember = or.GetBestCandidateWithSubstitutedTypeArguments(); } else @@ -1811,61 +1952,61 @@ namespace ICSharpCode.Decompiler.CSharp EmptyList.Instance, isInvocation: false) as MemberResolveResult; if (result == null || result.IsError) - return false; + return OverloadResolutionErrors.AmbiguousMatch; foundMember = result.Member; } } - return foundMember != null && IsAppropriateCallTarget(expectedTargetDetails, method.AccessorOwner, foundMember); + if (foundMember == null || !IsAppropriateCallTarget(expectedTargetDetails, method.AccessorOwner!, foundMember)) + { + foundMember = null; + return OverloadResolutionErrors.AmbiguousMatch; + } + return OverloadResolutionErrors.None; + } + + /// + /// Whether the accessor's last parameter is the assigned value: a setter takes it, and so do + /// the two event accessors, written as += and -=. + /// + static bool TakesAssignedValueLast(IMethod method) + { + return method.AccessorKind is System.Reflection.MethodSemanticsAttributes.Setter + or System.Reflection.MethodSemanticsAttributes.Adder + or System.Reflection.MethodSemanticsAttributes.Remover; + } + + /// + /// Whether the accessor is written as a property or indexer access. One with more parameters + /// than the access syntax has room for is written as a call, assigned value and all. + /// + static bool IsWrittenAsMemberAccess(IMethod method) + { + if (!method.IsAccessor) + return false; + if (method.AccessorOwner!.SymbolKind == SymbolKind.Indexer) + return true; + return method.Parameters.Count == (TakesAssignedValueLast(method) ? 1 : 0); } ExpressionWithResolveResult HandleAccessorCall(ExpectedTargetDetails expectedTargetDetails, IMethod method, - TranslatedExpression target, List arguments, string[]? argumentNames) + TranslatedExpression target, ArgumentList argumentList) { - bool requireTarget; - if (settings.AlwaysQualifyMemberReferences || method.AccessorOwner!.SymbolKind == SymbolKind.Indexer || expressionBuilder.HidesVariableWithName(method.AccessorOwner.Name)) - requireTarget = true; - else if (method.IsStatic) - requireTarget = !expressionBuilder.IsCurrentOrContainingType(method.DeclaringTypeDefinition); - else - requireTarget = !(target.Expression is ThisReferenceExpression); - bool targetCasted = false; - bool isSetter = method.ReturnType.IsKnownType(KnownTypeCode.Void); - bool argumentsCasted = (isSetter && method.Parameters.Count == 1) || (!isSetter && method.Parameters.Count == 0); - var targetResolveResult = requireTarget ? target.ResolveResult : null; + bool isSetter = argumentList.IsSetter; - TranslatedExpression value = default(TranslatedExpression); - if (isSetter) + // Dropping every argument would turn an indexer access into a property access. + if (argumentList.FirstOptionalArgumentIndex == 0 && method.AccessorOwner!.SymbolKind == SymbolKind.Indexer) { - value = arguments.Last(); - arguments.Remove(value); + argumentList.FirstOptionalArgumentIndex = 1; } - IMember? foundMember; - while (!IsUnambiguousAccess(expectedTargetDetails, targetResolveResult, method, arguments, argumentNames, out foundMember)) - { - if (!argumentsCasted) - { - argumentsCasted = true; - CastArguments(arguments, method.Parameters.ToList()); - } - else if (!requireTarget) - { - requireTarget = true; - targetResolveResult = target.ResolveResult; - } - else if (!targetCasted) - { - targetCasted = true; - target = target.ConvertTo(method.AccessorOwner!.DeclaringType, expressionBuilder); - targetResolveResult = target.ResolveResult; - } - else - { - foundMember = method.AccessorOwner!; - break; - } - } + var transform = GetRequiredTransformations(expectedTargetDetails, method, ref target, ref argumentList, + CallTransformation.RequireTarget, writtenAsMemberAccess: true, out var foundMember); + Debug.Assert(foundMember != null); + bool requireTarget = (transform & CallTransformation.RequireTarget) != 0; + var arguments = argumentList.GetArgumentExpressions().ToList(); + // Not one of the arguments the ladder casts. + TranslatedExpression value = isSetter ? argumentList.Arguments[argumentList.Length - 1] : default; var rr = new MemberResolveResult(target.ResolveResult, foundMember); if (isSetter) @@ -1874,7 +2015,7 @@ namespace ICSharpCode.Decompiler.CSharp if (arguments.Count != 0) { - expr = new IndexerExpression(target.ResolveResult is InitializedObjectResolveResult ? null : target.Expression, arguments.Select(a => a.Expression)) + expr = new IndexerExpression(target.ResolveResult is InitializedObjectResolveResult ? null : target.Expression, arguments) .WithoutILInstruction().WithRR(rr); } else if (requireTarget) @@ -1906,7 +2047,7 @@ namespace ICSharpCode.Decompiler.CSharp { if (arguments.Count != 0) { - return new IndexerExpression(target.Expression, arguments.Select(a => a.Expression)) + return new IndexerExpression(target.Expression, arguments) .WithoutILInstruction().WithRR(rr); } else if (requireTarget) @@ -1986,24 +2127,10 @@ namespace ICSharpCode.Decompiler.CSharp } else { - while (IsUnambiguousCall(expectedTargetDetails, method, null, Empty.Array, - argumentList.GetArgumentResolveResults().ToArray(), - argumentList.GetArgumentNames(), argumentList.FirstOptionalArgumentIndex, out _, - out var bestCandidateIsExpandedForm) != OverloadResolutionErrors.None || bestCandidateIsExpandedForm != argumentList.IsExpandedForm) - { - if (argumentList.AddNamesToPrimitiveValues) - { - argumentList.AddNamesToPrimitiveValues = false; - continue; - } - if (argumentList.FirstOptionalArgumentIndex >= 0) - { - argumentList.FirstOptionalArgumentIndex = -1; - continue; - } - CastArguments(argumentList.Arguments, argumentList.ExpectedParameters); - break; // make sure that we don't not end up in an infinite loop - } + // A constructor names its type, so neither qualification nor type arguments apply. + TranslatedExpression noTarget = default; + GetRequiredTransformations(expectedTargetDetails, method, ref noTarget, ref argumentList, + CallTransformation.None, writtenAsMemberAccess: false, out _); IType? returnTypeOverride = null; if (typeSystem.MainModule.TypeSystemOptions.HasFlag(TypeSystemOptions.NativeIntegersWithoutAttribute)) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs index 76a8646f6..ef2a17c75 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs @@ -28,13 +28,48 @@ namespace ICSharpCode.Decompiler.IL.Transforms public class NamedArgumentTransform : IStatementTransform { + /// + /// How many arguments may carry a name: a setter's last one is the assigned value, which is + /// written as the right-hand side. + /// + static int NameableArgumentCount(CallInstruction call) + { + if (call.Method.AccessorKind is System.Reflection.MethodSemanticsAttributes.Setter + or System.Reflection.MethodSemanticsAttributes.Adder + or System.Reflection.MethodSemanticsAttributes.Remover) + { + return call.Arguments.Count - 1; + } + return call.Arguments.Count; + } + internal static FindResult CanIntroduceNamedArgument(CallInstruction call, ILInstruction child, ILVariable v, ILInstruction expressionBeingMoved) { Debug.Assert(child.Parent == call); if (call.IsInstanceCall && child.ChildIndex == 0) return FindResult.Stop; // cannot use named arg to move expressionBeingMoved before this pointer - if (call.Method.IsOperator || call.Method.IsAccessor) - return FindResult.Stop; // cannot use named arg for operators or accessors + if (call.Method.IsOperator) + return FindResult.Stop; // cannot use named arg for operators + bool isIndexerSetter = false; + if (call.Method.IsAccessor) + { + // Only an indexer access has an argument list that can carry names. + if (call.Method.AccessorOwner!.SymbolKind != SymbolKind.Indexer) + return FindResult.Stop; + // A name replaces the call with a block: a call-inline-assign block is matched by + // the call it holds, and a compound assignment requires a call in its target. + if (call.Parent is Block { Kind: BlockKind.CallInlineAssign }) + return FindResult.Stop; + if (call.Parent is CompoundAssignmentInstruction { TargetKind: CompoundTargetKind.Property } compoundAssignment + && compoundAssignment.Target == call) + { + return FindResult.Stop; + } + // A setter's last argument is the assigned value, written as the right-hand side. + isIndexerSetter = call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Setter; + if (isIndexerSetter && child.ChildIndex == call.Arguments.Count - 1) + return FindResult.Stop; + } if (call.Method is VarArgInstanceMethod) return FindResult.Stop; // CallBuilder doesn't support named args when using varargs if (call.Method.IsConstructor) @@ -45,7 +80,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms } if (call.Method.Parameters.Any(p => string.IsNullOrEmpty(p.Name))) return FindResult.Stop; // cannot use named arguments - for (int i = child.ChildIndex; i < call.Arguments.Count; i++) + int nameableArgumentCount = isIndexerSetter ? call.Arguments.Count - 1 : call.Arguments.Count; + Debug.Assert(nameableArgumentCount == NameableArgumentCount(call)); + for (int i = child.ChildIndex; i < nameableArgumentCount; i++) { var r = ILInlining.FindLoadInNext(call.Arguments[i], v, expressionBeingMoved, InliningOptions.None); if (r.Type == FindResultType.Found) @@ -87,11 +124,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } } - foreach (var arg in call.Arguments) + // A block only holds what CanIntroduceNamedArgument admitted. + Debug.Assert(!call.Method.IsAccessor || call.Method.AccessorOwner!.SymbolKind == SymbolKind.Indexer); + int nameableArgumentCount = NameableArgumentCount(call); + for (int i = 0; i < nameableArgumentCount; i++) { - if (arg.MatchLdLoc(v)) + if (call.Arguments[i].MatchLdLoc(v)) { - return FindResult.NamedArgument(arg, arg); + return FindResult.NamedArgument(call.Arguments[i], call.Arguments[i]); } } return FindResult.Stop;