From 3c062bad0ce7e7d8aecbabcf63fe69dfba0b88d2 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 10 Jun 2026 20:07:30 +0200 Subject: [PATCH] Fix anonymous-type lambda early-return emitting unresolvable cast (#3752) * 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 | 28 ++++++++++--------- 3 files changed, 49 insertions(+), 13 deletions(-) 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 36b7ffa38..69300d285 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -936,6 +936,12 @@ namespace ICSharpCode.Decompiler.Tests 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 000000000..536f859fe --- /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 3c7ff756b..66c966602 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -382,6 +382,21 @@ namespace ICSharpCode.Decompiler.CSharp .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 @@ namespace ICSharpCode.Decompiler.CSharp 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 true; - return false; - } - protected internal override TranslatedStatement VisitThrow(Throw inst) { return new ThrowStatement(exprBuilder.Translate(inst.Argument)).WithILInstruction(inst);