Browse Source

Keep 'delegate {}' when a parameter type is inaccessible

The parameter-list-less anonymous method form is compatible with any
delegate signature, and C# code must rely on exactly that when a
delegate's parameter types cannot be named at the use site: IL, unlike
C#, permits a delegate signature to reference less accessible types.
Expanding such an anonymous method into a lambda would force the
unnameable type into a parameter list. Keep the delegate form, with its
parameter list dropped, when the parameters are unused and one of their
types is not accessible from the current context.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3975/head
Siegfried Pammer 1 month ago committed by Siegfried Pammer
parent
commit
60c08fcb74
  1. 3
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 6
      ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
  3. 31
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.cs
  4. 138
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.il
  5. 8
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs
  6. 65
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

3
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -105,6 +105,7 @@ @@ -105,6 +105,7 @@
<None Include="TestCases\ILPretty\ExtensionEncodingV2.il" />
<None Include="testcases\ilpretty\ExtensionEncodingV1.il" />
<None Include="TestCases\ILPretty\GuessAccessors.il" />
<None Include="TestCases\ILPretty\InaccessibleParameterTypes.il" />
<None Include="TestCases\ILPretty\Issue2260SwitchString.il" />
<None Include="TestCases\ILPretty\Issue3442.il" />
<None Include="TestCases\ILPretty\Issue3344CkFinite.il" />
@ -209,6 +210,8 @@ @@ -209,6 +210,8 @@
<None Include="TestCases\ILPretty\FSharpUsing_Release.cs" />
<Compile Remove="TestCases\ILPretty\GuessAccessors.cs" />
<None Include="TestCases\ILPretty\GuessAccessors.cs" />
<Compile Remove="TestCases\ILPretty\InaccessibleParameterTypes.cs" />
<None Include="TestCases\ILPretty\InaccessibleParameterTypes.cs" />
<Compile Remove="TestCases\ILPretty\NoAccessorProperties.cs" />
<None Include="TestCases\ILPretty\NoAccessorProperties.cs" />
<Compile Remove="TestCases\ILPretty\Issue1145.cs" />

6
ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs

@ -359,6 +359,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -359,6 +359,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run();
}
[Test]
public async Task InaccessibleParameterTypes()
{
await Run();
}
[Test]
public async Task EmptyBodies()
{

31
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty
{
public class InaccessibleParameterTypes
{
private class Hidden
{
}
public delegate void Handler(Hidden h);
public static void Register(Action<Hidden> callback)
{
}
}
public class InaccessibleParameterTypesConsumer
{
public InaccessibleParameterTypes.Handler Create()
{
return delegate {
};
}
public void Run()
{
InaccessibleParameterTypes.Register(delegate {
});
}
}
}

138
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.il

@ -0,0 +1,138 @@ @@ -0,0 +1,138 @@
// Anonymous methods whose delegate signature contains a type the use site cannot name.
// IL (unlike C#) permits a public delegate with a less-accessible parameter type, so the
// parameter-list-less "delegate {}" form is the only C# syntax the consumer class below
// could legally have used - the decompiler must not expand it to a lambda parameter list.
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 4:0:0:0
}
.assembly extern System.Core
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 4:0:0:0
}
.assembly InaccessibleParameterTypes
{
.ver 1:0:0:0
}
.module InaccessibleParameterTypes.dll
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00020003 // ILONLY 32BITPREFERRED
.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes
extends [mscorlib]System.Object
{
.class nested private auto ansi beforefieldinit Hidden
extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
.class nested public auto ansi sealed Handler
extends [mscorlib]System.MulticastDelegate
{
.method public hidebysig specialname rtspecialname instance void .ctor (object 'object', native int 'method') runtime managed
{
}
.method public hidebysig newslot virtual instance void Invoke (class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden h) runtime managed
{
}
}
.method public hidebysig static void Register (class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> callback) cil managed
{
.maxstack 8
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer
extends [mscorlib]System.Object
{
// Fields
.field private static class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler '<>f__am$cache0'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
.field private static class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> '<>f__am$cache1'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
// Methods
.method public hidebysig instance class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler Create () cil managed
{
.maxstack 8
ldsfld class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache0'
brtrue.s IL_0016
ldnull
ldftn void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<Create>m__0'(class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden)
newobj instance void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler::.ctor(object, native int)
stsfld class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache0'
IL_0016: ldsfld class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache0'
ret
}
.method public hidebysig instance void Run () cil managed
{
.maxstack 8
ldsfld class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache1'
brtrue.s IL_0016
ldnull
ldftn void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<Run>m__1'(class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden)
newobj instance void class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden>::.ctor(object, native int)
stsfld class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache1'
IL_0016: ldsfld class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache1'
call void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes::Register(class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden>)
ret
}
.method private hidebysig static void '<Create>m__0' (class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden '') cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
.maxstack 8
ret
}
.method private hidebysig static void '<Run>m__1' (class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden '') cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
.maxstack 8
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}

8
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs

@ -163,11 +163,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.DelegateConstruction @@ -163,11 +163,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.DelegateConstruction
return (int x) => this.x;
}
#if ROSLYN
// Roslyn names the parameters of an anonymous method declared without a parameter
// list "<p0>", which no lambda parameter list can spell, so the delegate form is
// kept. Legacy csc names them "param0" - a perfectly good identifier that does
// become a lambda - hence the Roslyn-only guard.
public Action<object> Bug971_DelegateWithoutParameterList()
{
return (object obj) => {
return delegate {
};
}
#endif
private void DoAction(Action action)
{

65
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -2537,6 +2537,12 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2537,6 +2537,12 @@ namespace ICSharpCode.Decompiler.CSharp
attributeSections.Add(new AttributeSection(astBuilder.ConvertAttribute(attr)) { AttributeTarget = "return" });
}
bool parametersAreUsed = (
from ident in body.Descendants.OfType<IdentifierExpression>()
let v = ident.GetILVariable()
where v != null && v.Function == function && v.Kind == VariableKind.Parameter
select ident).Any();
bool isLambda = false;
if (ame.Parameters.Any(p => p.Type is null))
{
@ -2548,19 +2554,19 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2548,19 +2554,19 @@ namespace ICSharpCode.Decompiler.CSharp
// C# 10 lambdas can have attributes, but anonymous methods cannot
isLambda = true;
}
else if (settings.UseLambdaSyntax && ame.Parameters.All(p => p.ParameterModifier == ReferenceKind.None && !p.IsParams))
else if (settings.UseLambdaSyntax && ame.Parameters.All(p => p.ParameterModifier == ReferenceKind.None && !p.IsParams)
&& (parametersAreUsed || (ParameterTypesAreAccessible(function) && ParametersAreNameable(function))))
{
// Lambdas cover statement bodies too; anonymous method syntax remains only for
// parameter shapes a lambda cannot express (ref/out/in and params modifiers).
// Lambdas cover statement bodies too. Anonymous method syntax remains where
// dropping the parameter list is the better rendering: for ref/out/in and
// params parameters (expressible in an explicitly typed lambda list, but
// conservatively left alone), and for unused parameters that a list would have
// to name or type from nothing to keep. The parameter-list-less "delegate {}"
// form is compatible with any delegate signature, so it is always legal there.
isLambda = true;
}
// Remove the parameter list from an AnonymousMethodExpression if the parameters are not used in the method body
var parameterReferencingIdentifiers =
from ident in body.Descendants.OfType<IdentifierExpression>()
let v = ident.GetILVariable()
where v != null && v.Function == function && v.Kind == VariableKind.Parameter
select ident;
if (!isLambda && !parameterReferencingIdentifiers.Any())
if (!isLambda && !parametersAreUsed)
{
ame.Parameters.Clear();
}
@ -2608,6 +2614,47 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2608,6 +2614,47 @@ namespace ICSharpCode.Decompiler.CSharp
.WithRR(new ConversionResolveResult(delegateType, rr, LambdaConversion.Instance));
}
/// <summary>
/// True when every parameter carries a name that can be written out as-is. Unused
/// parameters whose metadata names are missing or not identifiers (ilasm's synthetic
/// A_0/A_1 for unnamed Param rows, "&lt;p0&gt;" from an anonymous method declared without a
/// parameter list, obfuscated names) would have to be invented for a lambda's mandatory
/// parameter list; "delegate {}" drops the list instead of presenting a made-up name as
/// if it came from the source.
/// </summary>
static bool ParametersAreNameable(ILFunction function)
{
return function.Parameters.All(
p => !string.IsNullOrWhiteSpace(p.Name) && AssignVariableNames.IsValidName(p.Name));
}
bool ParameterTypesAreAccessible(ILFunction function)
{
var currentTypeDefinition = resolver.CurrentTypeDefinition;
if (currentTypeDefinition == null)
return true;
var lookup = new MemberLookup(currentTypeDefinition, currentTypeDefinition.ParentModule);
return function.Parameters.All(p => IsAccessible(p.Type));
bool IsAccessible(IType type)
{
switch (type)
{
case ParameterizedType pt:
return IsAccessible(pt.GenericType) && pt.TypeArguments.All(IsAccessible);
case TypeWithElementType t:
return IsAccessible(t.ElementType);
default:
for (var td = type.GetDefinition(); td != null; td = td.DeclaringTypeDefinition)
{
if (!lookup.IsAccessible(td, allowProtectedAccess: true))
return false;
}
return true;
}
}
}
protected internal override TranslatedExpression VisitILFunction(ILFunction function, TranslationContext context)
{
return TranslateFunction(function.DelegateType!, function)

Loading…
Cancel
Save