From 9e281c5b0d30106bc54e8c248298a69aa6fe8bc8 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Thu, 4 Jun 2026 22:01:53 +0200 Subject: [PATCH 1/2] Fix anonymous-type lambda early-return emitting unresolvable cast When a lambda's inferred return type contains an anonymous type and one branch returns null, the decompiler emitted an explicit cast such as `return (IEnumerable<<>f__AnonymousType0>)null;`, which is invalid C#. Skip the cast in IsPossibleLossOfTypeInformation for null literals whenever the expected type contains an anonymous type: null is implicitly convertible to any reference type, so no cast is needed, and the anonymous type has no nameable form to cast to anyway. Fixes #3751 --- .../PrettyTestRunner.cs | 6 ++++ .../TestCases/Pretty/Issue3751.cs | 28 +++++++++++++++++++ .../CSharp/StatementBuilder.cs | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3751.cs diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 36b7ffa386..69300d285b 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -936,6 +936,12 @@ public async Task Issue3684([ValueSource(nameof(roslyn4OrNewerOptions))] Compile await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task Issue3751([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + async Task RunForLibrary([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action configureDecompiler = null) { await Run(testName, asmOptions | AssemblerOptions.Library, cscOptions | CompilerOptions.Library, configureDecompiler); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3751.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3751.cs new file mode 100644 index 0000000000..536f859fec --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3751.cs @@ -0,0 +1,28 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class Issue3751 + { + private static bool Cond; + + private static T Infer(Func factory) + { + return factory(); + } + + public object Trigger() + { + return Infer(delegate { + if (Cond) + { + Console.WriteLine(); + return null; + } + return new { + Value = 1 + }; + }); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index 3c7ff756b8..5598baf3e6 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -412,7 +412,7 @@ private bool IsPossibleLossOfTypeInformation(IType givenType, IType expectedType if (expectedType == SpecialType.Dynamic) return true; if (givenType == SpecialType.NullType) - return true; + return !expectedType.ContainsAnonymousType(); return false; } From 95b5fb0c84581505338cd68767c4f7933d367b23 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Fri, 5 Jun 2026 08:29:51 +0200 Subject: [PATCH 2/2] address review comment --- .../CSharp/StatementBuilder.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index 5598baf3e6..66c9666028 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -382,6 +382,21 @@ protected internal override TranslatedStatement VisitLeave(Leave inst) .WithRR(new ConversionResolveResult(currentResultType, expr.ResolveResult, Conversion.IdentityConversion)).WithoutILInstruction(); } return new ReturnStatement(expr).WithILInstruction(inst); + + static bool IsPossibleLossOfTypeInformation(IType givenType, IType expectedType) + { + if (expectedType.ContainsAnonymousType()) + return false; + if (NormalizeTypeVisitor.IgnoreNullability.EquivalentTypes(givenType, expectedType)) + return false; + if (expectedType is TupleType { ElementNames.IsEmpty: false }) + return true; + if (expectedType == SpecialType.Dynamic) + return true; + if (givenType == SpecialType.NullType) + return true; + return false; + } } else return new ReturnStatement().WithILInstruction(inst); @@ -403,19 +418,6 @@ protected internal override TranslatedStatement VisitLeave(Leave inst) return new GotoStatement(label).WithILInstruction(inst); } - private bool IsPossibleLossOfTypeInformation(IType givenType, IType expectedType) - { - if (NormalizeTypeVisitor.IgnoreNullability.EquivalentTypes(givenType, expectedType)) - return false; - if (expectedType is TupleType { ElementNames.IsEmpty: false }) - return true; - if (expectedType == SpecialType.Dynamic) - return true; - if (givenType == SpecialType.NullType) - return !expectedType.ContainsAnonymousType(); - return false; - } - protected internal override TranslatedStatement VisitThrow(Throw inst) { return new ThrowStatement(exprBuilder.Translate(inst.Argument)).WithILInstruction(inst);