From d1b8cd646b598996e3977e1a2415ebe2146dafd0 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 6 Sep 2026 11:05:26 +0200 Subject: [PATCH] Expand a params argument to the parameter's element type The parameters standing in for the expanded arguments were built from the element type of the array the compiler had built, not from the element type the params collection declares. The two can only differ where the collection is covariant in its element type, and no compiler emits that shape today - it materializes the array into a local of the target type first, which the pattern no longer matches - so this only removes the dependency on that. Where the params collection is one overload resolution cannot unpack, the expanded form is now abandoned instead of being built from a type that resolution would never have used. Assisted-by: Claude:claude-opus-5:Claude Code --- ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 39 ++++++++++++-------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index ccbacb6a6..1836d8b7f 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -1128,7 +1128,7 @@ namespace ICSharpCode.Decompiler.CSharp ref List arguments) { var expressionBuilder = this.expressionBuilder; - if (ExtractArguments(out var elementType, out var expandedParameters, out var expandedArguments)) + if (ExtractArguments(out var expandedParameters, out var expandedArguments)) { expandedParameters.InsertRange(0, expectedParameters); expandedArguments.InsertRange(0, arguments); @@ -1144,18 +1144,30 @@ namespace ICSharpCode.Decompiler.CSharp } return false; - bool ExtractArguments([NotNullWhen(true)] out IType? elementType, [NotNullWhen(true)] out List? parameters, [NotNullWhen(true)] out List? arguments) + bool ExtractArguments([NotNullWhen(true)] out List? parameters, [NotNullWhen(true)] out List? arguments) { - elementType = null; parameters = null; arguments = null; + // Every expanded argument binds to the element type of the params collection, so + // that is the type the parameters standing in for them carry. The argument itself + // may be an array of a more derived element type, because the collection is + // covariant in it. Unpack it the same way overload resolution does, and give up + // where overload resolution would give up on the expanded form as well. + IType paramsElementType; + if (parameter.Type is ArrayType { Dimensions: 1 } paramsArray) + paramsElementType = paramsArray.ElementType; + else if (parameter.Type.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) + || parameter.Type.IsKnownType(KnownTypeCode.SpanOfT) + || parameter.Type.IsArrayInterfaceType()) + paramsElementType = parameter.Type.TypeArguments[0]; + else + return false; switch (paramsArgument.ResolveResult) { case CSharpInvocationResolveResult { Member: IMethod method, Arguments: var args }: // match System.Array.Empty() - if (args is [] && method is { IsStatic: true, FullName: "System.Array.Empty", TypeArguments: [var type] }) + if (args is [] && method is { IsStatic: true, FullName: "System.Array.Empty", TypeArguments: [_] }) { - elementType = type; arguments = new(); parameters = new(); return true; @@ -1170,33 +1182,30 @@ namespace ICSharpCode.Decompiler.CSharp && declaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) && paramType.Equals(type2)) { - elementType = type2; arguments = new() { new TranslatedExpression(oce.Arguments.Single()) }; - parameters = new() { new DefaultParameter(type2, string.Empty) }; + parameters = new() { new DefaultParameter(paramsElementType, string.Empty) }; return true; } return false; - case ArrayCreateResolveResult { Type: ArrayType { ElementType: var type3 }, SizeArguments: [{ ConstantValue: int arrayLength }] }: - elementType = type3; + case ArrayCreateResolveResult { SizeArguments: [{ ConstantValue: int arrayLength }] }: arguments = new(((ArrayCreateExpression)paramsArgument.Expression).Initializer?.Elements.Select(e => new TranslatedExpression(e)) ?? []); parameters = new List(arrayLength); for (int i = 0; i < arrayLength; i++) { - parameters.Add(new DefaultParameter(type3, string.Empty)); + parameters.Add(new DefaultParameter(paramsElementType, string.Empty)); if (arguments.Count <= i) - arguments.Add(new TranslatedExpression(expressionBuilder.GetDefaultValueExpression(type3).WithoutILInstruction())); + arguments.Add(new TranslatedExpression(expressionBuilder.GetDefaultValueExpression(paramsElementType).WithoutILInstruction())); } return true; - case ConversionResolveResult { Conversion.IsImplicitSpanConversion: true, Input: ArrayCreateResolveResult { Type: ArrayType { ElementType: var type3 }, SizeArguments: [{ ConstantValue: int arrayLength }] } }: - elementType = type3; + case ConversionResolveResult { Conversion.IsImplicitSpanConversion: true, Input: ArrayCreateResolveResult { SizeArguments: [{ ConstantValue: int arrayLength }] } }: var expr = paramsArgument.Expression is CastExpression cast ? cast.Expression : paramsArgument.Expression; arguments = new(((ArrayCreateExpression)expr).Initializer?.Elements.Select(e => new TranslatedExpression(e)) ?? []); parameters = new List(arrayLength); for (int i = 0; i < arrayLength; i++) { - parameters.Add(new DefaultParameter(type3, string.Empty)); + parameters.Add(new DefaultParameter(paramsElementType, string.Empty)); if (arguments.Count <= i) - arguments.Add(new TranslatedExpression(expressionBuilder.GetDefaultValueExpression(type3).WithoutILInstruction())); + arguments.Add(new TranslatedExpression(expressionBuilder.GetDefaultValueExpression(paramsElementType).WithoutILInstruction())); } return true; default: