diff --git a/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue2199.cs b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue2199.cs new file mode 100644 index 000000000..8a5e19425 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue2199.cs @@ -0,0 +1,35 @@ +using System; +using System.Runtime.CompilerServices; + +public class Issue2199 +{ + private class Item + { + public string Text; + } + + public event EventHandler Click; + + public event EventHandler DoubleClick; + + public void Register(string value) + { + Item item = new Item(); + item.Text = value.Trim(); +#if EXPECTED_OUTPUT + Click += [SpecialName] () => { + Console.WriteLine(item.Text); + }; + DoubleClick += [SpecialName] () => { + Console.WriteLine(item.Text); + }; +#else + Click += delegate { + Console.WriteLine(item.Text); + }; + DoubleClick += delegate { + Console.WriteLine(item.Text); + }; +#endif + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue2199.vb b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue2199.vb new file mode 100644 index 000000000..d57fc34fe --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue2199.vb @@ -0,0 +1,17 @@ +Imports System + +Public Class Issue2199 + Public Event Click As EventHandler + Public Event DoubleClick As EventHandler + + Private Class Item + Public Text As String + End Class + + Public Sub Register(value As String) + Dim item As New Item() + item.Text = value.Trim() + AddHandler Click, Sub() Console.WriteLine(item.Text) + AddHandler DoubleClick, Sub() Console.WriteLine(item.Text) + End Sub +End Class diff --git a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs index 3b3bcd78b..23f00f8aa 100644 --- a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs @@ -151,6 +151,12 @@ namespace ICSharpCode.Decompiler.Tests await Run(options: options | CompilerOptions.Library); } + [Test] + public async Task Issue2199([ValueSource(nameof(defaultOptions))] CompilerOptions options) + { + await Run(options: options | CompilerOptions.Library); + } + [Test] public async Task VBOnError([ValueSource(nameof(defaultOptions))] CompilerOptions options) { @@ -216,7 +222,7 @@ namespace ICSharpCode.Decompiler.Tests settings ??= new DecompilerSettings { FileScopedNamespaces = false, SortCustomAttributes = true }; var decompiled = await Tester.DecompileCSharp(executable.PathToAssembly, settings).ConfigureAwait(false); - CodeAssert.FilesAreEqual(csFile, decompiled, Tester.GetPreprocessorSymbols(options).ToArray()); + CodeAssert.FilesAreEqual(csFile, decompiled, Tester.GetPreprocessorSymbols(options).Append("EXPECTED_OUTPUT").ToArray()); Tester.RepeatOnIOError(() => File.Delete(decompiled)); } } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 3c5b14d09..7525e4f8c 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -478,6 +478,8 @@ namespace ICSharpCode.Decompiler.CSharp return true; if (settings.AnonymousTypes && type.IsAnonymousType(metadata)) return true; + if (settings.AnonymousMethods && type.IsDelegate(metadata) && name.StartsWith("VB$AnonymousDelegate_", StringComparison.Ordinal)) + return true; if (settings.Dynamic && type.IsDelegate(metadata) && (name.StartsWith("<>A", StringComparison.Ordinal) || name.StartsWith("<>F", StringComparison.Ordinal))) return true; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/DelegateConstruction.cs b/ICSharpCode.Decompiler/IL/Transforms/DelegateConstruction.cs index 7971c68e6..d449e5417 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/DelegateConstruction.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/DelegateConstruction.cs @@ -16,6 +16,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using System.Reflection.Metadata; @@ -195,6 +196,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms var ilReader = context.CreateILReader(); var body = context.PEFile.GetMethodBody(methodDefinition.RelativeVirtualAddress); var function = ilReader.ReadIL((MethodDefinitionHandle)targetMethod.MetadataToken, body, genericContext.Value, ILFunctionKind.Delegate, context.CancellationToken); + if (TryUnwrapVisualBasicDelegateWrapper(function, targetMethod, out var unwrappedMethod)) + { + targetMethod = unwrappedMethod; + methodDefinition = context.PEFile.Metadata.GetMethodDefinition((MethodDefinitionHandle)targetMethod.MetadataToken); + if (!methodDefinition.HasBody()) + return null; + genericContext = GenericContextFromTypeArguments(targetMethod.Substitution); + if (genericContext == null) + return null; + body = context.PEFile.GetMethodBody(methodDefinition.RelativeVirtualAddress); + function = ilReader.ReadIL((MethodDefinitionHandle)targetMethod.MetadataToken, body, genericContext.Value, ILFunctionKind.Delegate, context.CancellationToken); + } function.DelegateType = delegateType; // Embed the lambda into the parent function's ILAst, so that "Show steps" can show // how the lambda body is being transformed. @@ -215,6 +228,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms nestedContext.StepStartGroup("DelegateConstruction (nested lambdas)", function); ((IILTransform)this).Run(function, nestedContext); nestedContext.StepEndGroup(); + if (TryUnwrapTransformedVisualBasicDelegateWrapper(function, delegateType, out var unwrappedFunction)) + { + function.ReplaceWith(unwrappedFunction); + function = unwrappedFunction; + } function.AddILRange(target); function.AddILRange(value); if (value is Call call) @@ -222,6 +240,64 @@ namespace ICSharpCode.Decompiler.IL.Transforms return function; } + static bool TryUnwrapTransformedVisualBasicDelegateWrapper(ILFunction function, IType delegateType, out ILFunction unwrappedFunction) + { + unwrappedFunction = null; + if (function.Body is not BlockContainer container) + return false; + var block = container.EntryPoint; + if (block.Instructions.Count != 2 || !block.Instructions[1].MatchLeave(container, out _)) + return false; + if (block.Instructions[0] is not CallVirt callVirt) + return false; + if (callVirt.Method.Name != "Invoke" || callVirt.Method.DeclaringType.Kind != TypeKind.Delegate) + return false; + if (callVirt.Arguments.Count != 1 || callVirt.Arguments[0] is not ILFunction innerFunction) + return false; + if (innerFunction.DelegateType?.Name.StartsWith("VB$AnonymousDelegate_", StringComparison.Ordinal) != true) + return false; + innerFunction.DelegateType = delegateType; + unwrappedFunction = innerFunction; + return true; + } + + static bool TryUnwrapVisualBasicDelegateWrapper(ILFunction function, IMethod wrapperMethod, out IMethod unwrappedMethod) + { + unwrappedMethod = null; + if (!wrapperMethod.Name.Contains("$__R", StringComparison.Ordinal)) + return false; + var thisVariable = function.Variables.SingleOrDefault(VariableKindExtensions.IsThis); + if (thisVariable == null || function.Body is not BlockContainer container) + return false; + var block = container.EntryPoint; + if (block.Instructions.Count != 2 || !block.Instructions[1].MatchLeave(container, out _)) + return false; + if (block.Instructions[0] is Call call) + { + if (call.Arguments.Count != 1 || !call.Arguments[0].MatchLdLoc(thisVariable)) + return false; + unwrappedMethod = call.Method; + } + else if (block.Instructions[0] is CallVirt callVirt) + { + if (callVirt.Method.Name != "Invoke" || callVirt.Method.DeclaringType.Kind != TypeKind.Delegate) + return false; + if (callVirt.Arguments.Count != 1 || !MatchDelegateConstruction(callVirt.Arguments[0], out unwrappedMethod, out var target, out _)) + return false; + if (!target.MatchLdLoc(thisVariable)) + return false; + } + else + { + return false; + } + if (!object.Equals(unwrappedMethod.DeclaringTypeDefinition, wrapperMethod.DeclaringTypeDefinition)) + return false; + if (!IsAnonymousMethod(wrapperMethod.DeclaringTypeDefinition, unwrappedMethod)) + return false; + return true; + } + private static bool ValidateDelegateTarget(ILInstruction inst) { switch (inst) @@ -238,6 +314,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms // TODO : ldfld chains must be validated more thoroughly, i.e., we should make sure // that the value of the field is never changed. ILInstruction target = ldobj; + // Match delegate targets stored in nested display classes, e.g. + // ldobj(ldflda field(ldobj(ldflda field(ldloc displayClass)))). while (target is LdObj || target is LdFlda) { if (target is LdObj o) @@ -297,6 +375,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms break; case LdObj lo: ILInstruction inner = lo.Target; + // Match the same display-class field chain after ValidateDelegateTarget + // accepted it: ldobj(ldflda field(ldflda field(ldloc displayClass))). while (inner is LdFlda ldf) { inner = ldf.Target; diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs index ff6bede9a..837349303 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs @@ -466,7 +466,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms { try { - if (method.Parameters.Count != 0) + if (method.Parameters.Count > 1) return false; var handle = (MethodDefinitionHandle)method.MetadataToken; var module = (MetadataModule)method.ParentModule; @@ -489,20 +489,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms // IL_0000: ldarg.0 // IL_0001: call instance void [mscorlib]System.Object::.ctor() // IL_0006: ret - var opCode = DecodeOpCodeSkipNop(ref reader); - switch (opCode) - { - case ILOpCode.Ldarg: - case ILOpCode.Ldarg_s: - if (reader.DecodeIndex(opCode) != 0) - return false; - break; - case ILOpCode.Ldarg_0: - // OK - break; - default: - return false; - } + if (!DecodeLdArg(ref reader, 0)) + return false; if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Call) return false; var baseCtorHandle = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32()); @@ -513,7 +501,43 @@ namespace ICSharpCode.Decompiler.IL.Transforms return false; if (!objectCtor.IsConstructor || objectCtor.Parameters.Count != 0) return false; - return DecodeOpCodeSkipNop(ref reader) == ILOpCode.Ret; + if (method.Parameters.Count == 0) + return DecodeOpCodeSkipNop(ref reader) == ILOpCode.Ret; + if (!object.Equals(method.Parameters[0].Type.GetDefinition(), method.DeclaringTypeDefinition)) + return false; + if (!DecodeLdArg(ref reader, 1)) + return false; + switch (DecodeOpCodeSkipNop(ref reader)) + { + case ILOpCode.Brfalse: + reader.ReadInt32(); + break; + case ILOpCode.Brfalse_s: + reader.ReadSByte(); + break; + default: + return false; + } + bool copiedField = false; + while (true) + { + var opCode = DecodeOpCodeSkipNop(ref reader); + if (opCode == ILOpCode.Ret) + return copiedField; + if (!DecodeLdArg(opCode, ref reader, 0)) + return false; + if (!DecodeLdArg(ref reader, 1)) + return false; + if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Ldfld) + return false; + var sourceField = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32()); + if (DecodeOpCodeSkipNop(ref reader) != ILOpCode.Stfld) + return false; + var targetField = MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32()); + if (sourceField.IsNil || sourceField != targetField) + return false; + copiedField = true; + } } catch (BadImageFormatException) { @@ -521,6 +545,27 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + static bool DecodeLdArg(ref BlobReader reader, int expectedIndex) + { + return DecodeLdArg(DecodeOpCodeSkipNop(ref reader), ref reader, expectedIndex); + } + + static bool DecodeLdArg(ILOpCode opCode, ref BlobReader reader, int expectedIndex) + { + switch (opCode) + { + case ILOpCode.Ldarg: + case ILOpCode.Ldarg_s: + return reader.DecodeIndex(opCode) == expectedIndex; + case ILOpCode.Ldarg_0: + return expectedIndex == 0; + case ILOpCode.Ldarg_1: + return expectedIndex == 1; + default: + return false; + } + } + static ILOpCode DecodeOpCodeSkipNop(ref BlobReader reader) { ILOpCode code;