diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 714454f75..d09ee4225 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -218,6 +218,14 @@ + + + + + + + + diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index 7e9f8df94..70cf2bc8c 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -429,6 +429,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..412d61282 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs @@ -32,6 +32,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness Generics(); ConstructorTest(); TestIndexer(); + TestIndexerWithNamedArguments(); Issue1281(); Issue1747(); CallAmbiguousOutParam(); @@ -330,6 +331,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness } #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 +620,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..4fed08cde 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NamedArguments.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NamedArguments.cs @@ -60,6 +60,52 @@ 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 void Use(int a, int b, int c) { } @@ -81,5 +127,21 @@ 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 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..73ec58395 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs @@ -55,6 +55,83 @@ 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 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 +407,53 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty d(42); } #endif + + 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 d4cb8e090..c35e0e50a 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; } @@ -88,10 +91,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; @@ -132,7 +144,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 +512,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 +811,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 +1036,17 @@ 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 accessor written as an access takes its assigned value out of the argument + // list; one written as a call passes it like any other argument. The scan below and + // GetActualArgumentCount() have to agree on that, so it is decided once, here. + bool writtenAsMemberAccess = IsWrittenAsMemberAccess(method); + bool isSetter = writtenAsMemberAccess && TakesAssignedValueLast(method); + // A named argument of an indexer access names a parameter of the indexer, which the type + // system takes from the getter. The accessor being called may name the same parameters + // differently - C# cannot declare that, but other languages can. + 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 +1058,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 +1076,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 +1155,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 +1178,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; @@ -1309,7 +1353,7 @@ namespace ICSharpCode.Decompiler.CSharp bool skipTargetCast = 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, + argumentList.GetArgumentResolveResults().ToArray(), argumentList.GetArgumentNames(), out foundMethod, out var bestCandidateIsExpandedForm)) != OverloadResolutionErrors.None || bestCandidateIsExpandedForm != argumentList.IsExpandedForm) { switch (errors) @@ -1634,7 +1678,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; @@ -1645,10 +1689,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); @@ -1751,10 +1791,10 @@ namespace ICSharpCode.Decompiler.CSharp } bool IsUnambiguousAccess(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method, - IList arguments, string[]? argumentNames, [NotNullWhen(true)] out IMember? foundMember) + IList arguments, string[]? argumentNames, [NotNullWhen(true)] 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) @@ -1772,7 +1812,7 @@ namespace ICSharpCode.Decompiler.CSharp if (method.AccessorOwner!.SymbolKind == SymbolKind.Indexer) { var or = new OverloadResolution(resolver.Compilation, - arguments.SelectArray(a => a.ResolveResult), + arguments.ToArray(), argumentNames: argumentNames, typeArguments: Empty.Array, conversions: expressionBuilder.resolver.conversions); @@ -1797,8 +1837,32 @@ namespace ICSharpCode.Decompiler.CSharp return foundMember != null && IsAppropriateCallTarget(expectedTargetDetails, method.AccessorOwner, foundMember); } + /// + /// 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)) @@ -1808,24 +1872,32 @@ namespace ICSharpCode.Decompiler.CSharp else requireTarget = !(target.Expression is ThisReferenceExpression); bool targetCasted = false; - bool isSetter = method.ReturnType.IsKnownType(KnownTypeCode.Void); + bool isSetter = argumentList.IsSetter; bool argumentsCasted = (isSetter && method.Parameters.Count == 1) || (!isSetter && method.Parameters.Count == 0); var targetResolveResult = requireTarget ? target.ResolveResult : null; - 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)) + while (!IsUnambiguousAccess(expectedTargetDetails, targetResolveResult, method, + argumentList.GetArgumentResolveResults(), argumentList.GetArgumentNames(), out foundMember)) { - if (!argumentsCasted) + if (argumentList.FirstOptionalArgumentIndex >= 0) + { + // Unlike the casts below, writing the omitted arguments out again cannot change + // what the access means, so try that first. + argumentList.FirstOptionalArgumentIndex = -1; + } + else if (!argumentsCasted) { argumentsCasted = true; - CastArguments(arguments, method.Parameters.ToList()); + CastArguments( + new ArraySegment(argumentList.Arguments, 0, argumentList.GetActualArgumentCount()), + argumentList.ExpectedParameters); } else if (!requireTarget) { @@ -1845,6 +1917,10 @@ namespace ICSharpCode.Decompiler.CSharp } } + var arguments = argumentList.GetArgumentExpressions().ToList(); + // The assigned value is not one of the arguments the ladder casts, so nothing it could + // try makes an access resolve that fails over the value's type. + TranslatedExpression value = isSetter ? argumentList.Arguments[argumentList.Length - 1] : default; var rr = new MemberResolveResult(target.ResolveResult, foundMember); if (isSetter) @@ -1853,7 +1929,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) @@ -1885,7 +1961,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) @@ -1967,7 +2043,7 @@ namespace ICSharpCode.Decompiler.CSharp { while (IsUnambiguousCall(expectedTargetDetails, method, null, Empty.Array, argumentList.GetArgumentResolveResults().ToArray(), - argumentList.GetArgumentNames(), argumentList.FirstOptionalArgumentIndex, out _, + argumentList.GetArgumentNames(), out _, out var bestCandidateIsExpandedForm) != OverloadResolutionErrors.None || bestCandidateIsExpandedForm != argumentList.IsExpandedForm) { if (argumentList.AddNamesToPrimitiveValues) diff --git a/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs index 4c37a4afd..1cd87d871 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;