Skip to content

Commit 0116f24

Browse files
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
1 parent 5319969 commit 0116f24

5 files changed

Lines changed: 219 additions & 9 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
<None Include="TestCases\ILPretty\ExtensionEncodingV2.il" />
106106
<None Include="testcases\ilpretty\ExtensionEncodingV1.il" />
107107
<None Include="TestCases\ILPretty\GuessAccessors.il" />
108+
<None Include="TestCases\ILPretty\InaccessibleParameterTypes.il" />
108109
<None Include="TestCases\ILPretty\Issue2260SwitchString.il" />
109110
<None Include="TestCases\ILPretty\Issue3442.il" />
110111
<None Include="TestCases\ILPretty\Issue3344CkFinite.il" />
@@ -209,6 +210,8 @@
209210
<None Include="TestCases\ILPretty\FSharpUsing_Release.cs" />
210211
<Compile Remove="TestCases\ILPretty\GuessAccessors.cs" />
211212
<None Include="TestCases\ILPretty\GuessAccessors.cs" />
213+
<Compile Remove="TestCases\ILPretty\InaccessibleParameterTypes.cs" />
214+
<None Include="TestCases\ILPretty\InaccessibleParameterTypes.cs" />
212215
<Compile Remove="TestCases\ILPretty\NoAccessorProperties.cs" />
213216
<None Include="TestCases\ILPretty\NoAccessorProperties.cs" />
214217
<Compile Remove="TestCases\ILPretty\Issue1145.cs" />

ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,12 @@ public async Task GuessAccessors()
353353
await Run();
354354
}
355355

356+
[Test]
357+
public async Task InaccessibleParameterTypes()
358+
{
359+
await Run();
360+
}
361+
356362
[Test]
357363
public async Task EmptyBodies()
358364
{
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
3+
namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty
4+
{
5+
public class InaccessibleParameterTypes
6+
{
7+
private class Hidden
8+
{
9+
}
10+
11+
public delegate void Handler(Hidden h);
12+
13+
public static void Register(Action<Hidden> callback)
14+
{
15+
}
16+
}
17+
public class InaccessibleParameterTypesConsumer
18+
{
19+
public InaccessibleParameterTypes.Handler Create()
20+
{
21+
return delegate {
22+
};
23+
}
24+
25+
public void Run()
26+
{
27+
InaccessibleParameterTypes.Register(delegate {
28+
});
29+
}
30+
}
31+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Anonymous methods whose delegate signature contains a type the use site cannot name.
2+
// IL (unlike C#) permits a public delegate with a less-accessible parameter type, so the
3+
// parameter-list-less "delegate {}" form is the only C# syntax the consumer class below
4+
// could legally have used - the decompiler must not expand it to a lambda parameter list.
5+
.assembly extern mscorlib
6+
{
7+
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
8+
.ver 4:0:0:0
9+
}
10+
.assembly extern System.Core
11+
{
12+
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
13+
.ver 4:0:0:0
14+
}
15+
.assembly InaccessibleParameterTypes
16+
{
17+
.ver 1:0:0:0
18+
}
19+
.module InaccessibleParameterTypes.dll
20+
.imagebase 0x00400000
21+
.file alignment 0x00000200
22+
.stackreserve 0x00100000
23+
.subsystem 0x0003 // WINDOWS_CUI
24+
.corflags 0x00020003 // ILONLY 32BITPREFERRED
25+
26+
.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes
27+
extends [mscorlib]System.Object
28+
{
29+
.class nested private auto ansi beforefieldinit Hidden
30+
extends [mscorlib]System.Object
31+
{
32+
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
33+
{
34+
.maxstack 8
35+
36+
ldarg.0
37+
call instance void [mscorlib]System.Object::.ctor()
38+
ret
39+
}
40+
}
41+
42+
.class nested public auto ansi sealed Handler
43+
extends [mscorlib]System.MulticastDelegate
44+
{
45+
.method public hidebysig specialname rtspecialname instance void .ctor (object 'object', native int 'method') runtime managed
46+
{
47+
}
48+
49+
.method public hidebysig newslot virtual instance void Invoke (class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden h) runtime managed
50+
{
51+
}
52+
}
53+
54+
.method public hidebysig static void Register (class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> callback) cil managed
55+
{
56+
.maxstack 8
57+
58+
ret
59+
}
60+
61+
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
62+
{
63+
.maxstack 8
64+
65+
ldarg.0
66+
call instance void [mscorlib]System.Object::.ctor()
67+
ret
68+
}
69+
}
70+
71+
.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer
72+
extends [mscorlib]System.Object
73+
{
74+
// Fields
75+
.field private static class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler '<>f__am$cache0'
76+
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
77+
.field private static class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> '<>f__am$cache1'
78+
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
79+
80+
// Methods
81+
.method public hidebysig instance class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler Create () cil managed
82+
{
83+
.maxstack 8
84+
85+
ldsfld class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache0'
86+
brtrue.s IL_0016
87+
88+
ldnull
89+
ldftn void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<Create>m__0'(class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden)
90+
newobj instance void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler::.ctor(object, native int)
91+
stsfld class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache0'
92+
93+
IL_0016: ldsfld class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Handler ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache0'
94+
ret
95+
}
96+
97+
.method public hidebysig instance void Run () cil managed
98+
{
99+
.maxstack 8
100+
101+
ldsfld class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache1'
102+
brtrue.s IL_0016
103+
104+
ldnull
105+
ldftn void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<Run>m__1'(class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden)
106+
newobj instance void class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden>::.ctor(object, native int)
107+
stsfld class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden> ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypesConsumer::'<>f__am$cache1'
108+
109+
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'
110+
call void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes::Register(class [System.Core]System.Action`1<class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden>)
111+
ret
112+
}
113+
114+
.method private hidebysig static void '<Create>m__0' (class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden '') cil managed
115+
{
116+
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
117+
.maxstack 8
118+
119+
ret
120+
}
121+
122+
.method private hidebysig static void '<Run>m__1' (class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden '') cil managed
123+
{
124+
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
125+
.maxstack 8
126+
127+
ret
128+
}
129+
130+
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
131+
{
132+
.maxstack 8
133+
134+
ldarg.0
135+
call instance void [mscorlib]System.Object::.ctor()
136+
ret
137+
}
138+
}

ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2537,6 +2537,12 @@ internal ExpressionWithResolveResult TranslateFunction(IType delegateType, ILFun
25372537
attributeSections.Add(new AttributeSection(astBuilder.ConvertAttribute(attr)) { AttributeTarget = "return" });
25382538
}
25392539

2540+
bool parametersAreUsed = (
2541+
from ident in body.Descendants.OfType<IdentifierExpression>()
2542+
let v = ident.GetILVariable()
2543+
where v != null && v.Function == function && v.Kind == VariableKind.Parameter
2544+
select ident).Any();
2545+
25402546
bool isLambda = false;
25412547
if (ame.Parameters.Any(p => p.Type is null))
25422548
{
@@ -2548,19 +2554,18 @@ internal ExpressionWithResolveResult TranslateFunction(IType delegateType, ILFun
25482554
// C# 10 lambdas can have attributes, but anonymous methods cannot
25492555
isLambda = true;
25502556
}
2551-
else if (settings.UseLambdaSyntax && ame.Parameters.All(p => p.ParameterModifier == ReferenceKind.None && !p.IsParams))
2557+
else if (settings.UseLambdaSyntax && ame.Parameters.All(p => p.ParameterModifier == ReferenceKind.None && !p.IsParams)
2558+
&& (parametersAreUsed || ParameterTypesAreAccessible(function)))
25522559
{
2553-
// Lambdas cover statement bodies too; anonymous method syntax remains only for
2554-
// parameter shapes a lambda cannot express (ref/out/in and params modifiers).
2560+
// Lambdas cover statement bodies too; anonymous method syntax remains for
2561+
// parameter shapes a lambda cannot express (ref/out/in and params modifiers)
2562+
// and for unused parameters whose types the current context cannot name:
2563+
// only the parameter-list-less "delegate {}" form, which is compatible with
2564+
// any delegate signature, can legally occur in such code.
25552565
isLambda = true;
25562566
}
25572567
// Remove the parameter list from an AnonymousMethodExpression if the parameters are not used in the method body
2558-
var parameterReferencingIdentifiers =
2559-
from ident in body.Descendants.OfType<IdentifierExpression>()
2560-
let v = ident.GetILVariable()
2561-
where v != null && v.Function == function && v.Kind == VariableKind.Parameter
2562-
select ident;
2563-
if (!isLambda && !parameterReferencingIdentifiers.Any())
2568+
if (!isLambda && !parametersAreUsed)
25642569
{
25652570
ame.Parameters.Clear();
25662571
}
@@ -2608,6 +2613,33 @@ from ident in body.Descendants.OfType<IdentifierExpression>()
26082613
.WithRR(new ConversionResolveResult(delegateType, rr, LambdaConversion.Instance));
26092614
}
26102615

2616+
bool ParameterTypesAreAccessible(ILFunction function)
2617+
{
2618+
var currentTypeDefinition = resolver.CurrentTypeDefinition;
2619+
if (currentTypeDefinition == null)
2620+
return true;
2621+
var lookup = new MemberLookup(currentTypeDefinition, currentTypeDefinition.ParentModule);
2622+
return function.Parameters.All(p => IsAccessible(p.Type));
2623+
2624+
bool IsAccessible(IType type)
2625+
{
2626+
switch (type)
2627+
{
2628+
case ParameterizedType pt:
2629+
return IsAccessible(pt.GenericType) && pt.TypeArguments.All(IsAccessible);
2630+
case TypeWithElementType t:
2631+
return IsAccessible(t.ElementType);
2632+
default:
2633+
for (var td = type.GetDefinition(); td != null; td = td.DeclaringTypeDefinition)
2634+
{
2635+
if (!lookup.IsAccessible(td, allowProtectedAccess: true))
2636+
return false;
2637+
}
2638+
return true;
2639+
}
2640+
}
2641+
}
2642+
26112643
protected internal override TranslatedExpression VisitILFunction(ILFunction function, TranslationContext context)
26122644
{
26132645
return TranslateFunction(function.DelegateType!, function)

0 commit comments

Comments
 (0)