Browse Source

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
pull/4004/head
Christoph Wille 1 month ago
parent
commit
a4ef71bb76
  1. 10
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaOptionalAndParamsParameters.cs
  2. 5
      ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoLambdaOptionalAndParamsParameters.Expected.cs
  3. 44
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  4. 7
      ICSharpCode.Decompiler/CSharp/RequiredNamespaceCollector.cs

10
ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaOptionalAndParamsParameters.cs

@ -10,9 +10,15 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -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 @@ -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()

5
ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoLambdaOptionalAndParamsParameters.Expected.cs

@ -1,5 +1,4 @@ @@ -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 @@ -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 @@ -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;
}
}
}

44
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -2543,13 +2543,16 @@ namespace ICSharpCode.Decompiler.CSharp @@ -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 @@ -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;

7
ICSharpCode.Decompiler/CSharp/RequiredNamespaceCollector.cs

@ -144,13 +144,6 @@ namespace ICSharpCode.Decompiler.CSharp @@ -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);

Loading…
Cancel
Save