From a4ef71bb7643d92e174d53f8abcbe2158324feb8 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 15 Aug 2026 17:51:02 +0200 Subject: [PATCH] Only take lambda params and defaults from a synthesized delegate's Invoke Copying 'params' from every target delegate's Invoke onto the lambda broke the Newtonsoft.Json round-trip: each plain '(object[] args) => ...' bound to a 'params' delegate came back as '(params object[] args)', and below C# 12 the fallback rendered that as '([ParamArray] object[] args)', which no C# version compiles (CS0674, plus CS8400 for lambda attributes before C# 10). The lambda's own metadata is the faithful record for named delegate types: current Roslyn emits ParamArrayAttribute and the default value on the closure method exactly when the source spelled them, and where Roslyn 4.14 omits the attribute the plain parameter list is equivalent anyway. Only a compiler-synthesized delegate type has to be spelled through the lambda's declaration, so that is the one place Invoke is still consulted. Without a legal pre-C# 12 spelling, the modifiers are now dropped instead of being turned into attributes. Assisted-by: Claude:claude-fable-5:Claude Code --- .../LambdaOptionalAndParamsParameters.cs | 10 +++++ ...bdaOptionalAndParamsParameters.Expected.cs | 5 +-- .../CSharp/ExpressionBuilder.cs | 44 +++++++------------ .../CSharp/RequiredNamespaceCollector.cs | 7 --- 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaOptionalAndParamsParameters.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaOptionalAndParamsParameters.cs index b9fa0b055..110db46f9 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaOptionalAndParamsParameters.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaOptionalAndParamsParameters.cs @@ -10,9 +10,15 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty private int total; + // Roslyn 4.14 puts no ParamArrayAttribute on the lambda's own method, so for a named + // delegate type the 'params' cannot be recovered there; newer compilers record it. public ParamsAction ParamsStatementBody() { +#if ROSLYN5 || !EXPECTED_OUTPUT return (params int[] xs) => { +#else + return (int[] xs) => { +#endif total += xs.Length; Console.WriteLine(xs.Length); }; @@ -20,7 +26,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public ParamsAction ParamsExpressionBody() { +#if ROSLYN5 || !EXPECTED_OUTPUT return (params int[] xs) => Console.WriteLine(xs.Length); +#else + return (int[] xs) => Console.WriteLine(xs.Length); +#endif } public OptionalFunc OptionalStatementBody() diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoLambdaOptionalAndParamsParameters.Expected.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoLambdaOptionalAndParamsParameters.Expected.cs index 988cf2535..c2394285a 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoLambdaOptionalAndParamsParameters.Expected.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoLambdaOptionalAndParamsParameters.Expected.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly { @@ -13,7 +12,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly public ParamsAction ParamsLambda() { - return ([ParamArray] int[] xs) => { + return (int[] xs) => { total += xs.Length; Console.WriteLine(xs.Length); }; @@ -21,7 +20,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly public OptionalFunc OptionalLambda() { - return ([Optional][DefaultParameterValue(5)] int x) => x * 2; + return (int x) => x * 2; } } } diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 6cae69619..18f1208cf 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -2543,13 +2543,16 @@ namespace ICSharpCode.Decompiler.CSharp AnonymousMethodExpression ame = new AnonymousMethodExpression(); ame.IsAsync = function.IsAsync; ame.Parameters.AddRange(MakeParameters(function.Parameters, function)); - // 'params' and parameter default values are part of the delegate's signature, and it - // is the delegate's Invoke method that call sites bind against. An anonymous function's - // own method does not reliably carry them - Roslyn 4.14 emits no ParamArrayAttribute - // there - so take them from Invoke, keeping declaration and call sites consistent. - var invokeParameters = delegateType.GetDelegateInvokeMethod()?.Parameters; - if (invokeParameters?.Count == ame.Parameters.Count) - { + if (delegateType.IsAnonymousDelegate() + && delegateType.GetDelegateInvokeMethod()?.Parameters is { } invokeParameters + && invokeParameters.Count == ame.Parameters.Count) + { + // A compiler-synthesized delegate type exists only because the lambda declared + // 'params' or a parameter default value, and the lambda's own declaration is the + // only way to spell that type. Roslyn 4.14 does not put a ParamArrayAttribute on + // the lambda's method, so take both from the delegate's Invoke method, which + // always carries them. Named delegate types are left alone: there the lambda's + // own metadata decides, and a plain parameter list is valid either way. int parameterIndex = 0; foreach (var pd in ame.Parameters) { @@ -2611,29 +2614,14 @@ namespace ICSharpCode.Decompiler.CSharp if (!settings.LambdaOptionalAndParamsParameters || ame.Parameters.Any(p => p.Type is null)) { // 'params' and parameter default values are only legal on the explicitly typed - // parameter list of a lambda, and only since C# 12. Elsewhere, downgrade them to - // their underlying metadata attributes so the information is not lost. + // parameter list of a lambda, and only since C# 12. There is no other spelling: + // [ParamArray] cannot be written explicitly in any language version, and attributes + // on lambda parameters need C# 10. Drop them; the delegate's Invoke method still + // carries them at every call site. foreach (var p in ame.Parameters) { - if (p.IsParams) - { - p.IsParams = false; - p.Attributes.Add(new AttributeSection(new Syntax.Attribute { - Type = astBuilder.ConvertAttributeType(compilation.FindType(KnownAttribute.ParamArray)) - })); - } - if (p.DefaultExpression is { } defaultValue) - { - defaultValue.Detach(); - p.Attributes.Add(new AttributeSection(new Syntax.Attribute { - Type = astBuilder.ConvertAttributeType(compilation.FindType(KnownAttribute.Optional)) - })); - var defaultParameterValue = new Syntax.Attribute { - Type = astBuilder.ConvertAttributeType(compilation.FindType(KnownAttribute.DefaultParameterValue)) - }; - defaultParameterValue.Arguments.Add(defaultValue); - p.Attributes.Add(new AttributeSection(defaultParameterValue)); - } + p.IsParams = false; + p.DefaultExpression = null; } } bool isLambda = false; diff --git a/ICSharpCode.Decompiler/CSharp/RequiredNamespaceCollector.cs b/ICSharpCode.Decompiler/CSharp/RequiredNamespaceCollector.cs index edf305c5a..753f317db 100644 --- a/ICSharpCode.Decompiler/CSharp/RequiredNamespaceCollector.cs +++ b/ICSharpCode.Decompiler/CSharp/RequiredNamespaceCollector.cs @@ -144,13 +144,6 @@ namespace ICSharpCode.Decompiler.CSharp { HandleAttributes(param.GetAttributes()); CollectNamespacesForTypeReference(param.Type); - if (param.IsOptional) - { - // ExpressionBuilder.TranslateFunction may downgrade a default value - // on an anonymous-function parameter to [Optional] and - // [DefaultParameterValue(...)]; keep their namespace in the superset. - namespaces.Add(KnownAttribute.DefaultParameterValue.GetTypeName().Namespace); - } } HandleTypeParameters(partMethod.TypeParameters); HandleOverrides(part.GetMethodImplementations(module.metadata), module);