From 38203f829fcb2d08a6439e66f6fb9406cdfb4289 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 5 Jul 2026 22:00:23 +0200 Subject: [PATCH] Fix #3475: Emit 'true ? null : new { ... }' for null of anonymous type When a null literal is the only argument a generic type argument could be inferred from, and that type argument is an anonymous type, ILSpy used to drop the type arguments entirely (they cannot be written explicitly), producing 'Test(null)', which no longer compiles (CS0411). A conditional expression whose never-taken branch creates an instance of the anonymous type is the minimal C# expression that gives a null value that type, so the argument now carries enough information for type inference. The instance expression is obtained by translating a synthesized 'newobj' with 'default.value' arguments through the existing pipeline rather than assembling syntax by hand; the property values are typed defaults, since the IL only contains ldnull. Anonymous types occurring inside other constructed types (e.g. arrays) stay unexpressible and keep the previous output. Assisted-by: Claude:claude-fable-5:Claude Code --- .../TestCases/Pretty/AnonymousTypes.cs | 41 +++++++++++ ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 71 ++++++++++++++++++- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypes.cs index 6a79c601bb..1c85f92f9b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypes.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypes.cs @@ -133,5 +133,46 @@ private static object MemberProjection(Version v) Renamed = v.Minor }; } + + private void NullOfAnonymousType() + { + Identify(true ? null : new { + X = default(int), + Y = default(int) + }); + Identify(true ? null : new { + N = default(int), + S = (string)null + }); + Identify(true ? null : new { + Inner = new { + X = default(int) + } + }); + } + + private void NullOfAnonymousTypeNonGenericCall() + { +#if OPT + MakeAction(new { + X = 0 + })(null); +#else + var action = MakeAction(new { + X = 0 + }); + action(null); +#endif + } + + private static void Identify(T t) + { + Console.WriteLine(typeof(T).FullName); + } + + private static Action MakeAction(T template) + { + return null; + } } } diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 75dca021b0..1efe31af69 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -1201,9 +1201,22 @@ private CallTransformation GetRequiredTransformationsForCall(ExpectedTargetDetai // if necessary. if (!CanInferTypeArgumentsFromArguments(method, argumentList, expressionBuilder.typeInference)) { - requireTypeArguments = true; - typeArguments = method.TypeArguments.ToArray(); - appliedRequireTypeArgumentsShortcut = true; + if (settings.AnonymousTypes + && method.TypeArguments.Any(a => a.ContainsAnonymousType()) + && PinTypesOfNullArguments(argumentList) + && CanInferTypeArgumentsFromArguments(method, argumentList, expressionBuilder.typeInference)) + { + // Anonymous types cannot be written as explicit type arguments; instead the + // null arguments were rewritten so that all type arguments are inferable. + requireTypeArguments = false; + typeArguments = Empty.Array; + } + else + { + requireTypeArguments = true; + typeArguments = method.TypeArguments.ToArray(); + appliedRequireTypeArgumentsShortcut = true; + } } else { @@ -1378,6 +1391,58 @@ static bool CanInferTypeArgumentsFromArguments(IMethod method, ArgumentList argu return success; } + /// + /// C# has no syntax to spell out an anonymous type, so a null literal cannot be given such + /// a type with a cast. The minimal expression that produces a null value of an anonymous + /// type is a conditional expression whose never-taken branch creates an instance of the + /// type: true ? null : new { A = default(int) }. + /// Replaces null-literal arguments of an anonymous type with such an expression, so that + /// type arguments involving anonymous types (which cannot be written explicitly either) + /// become inferable from the arguments. + /// Returns true, if at least one argument was replaced. + /// + private bool PinTypesOfNullArguments(ArgumentList argumentList) + { + bool anyArgumentReplaced = false; + for (int i = 0; i < argumentList.Length; i++) + { + IType expectedType = argumentList.ExpectedParameters[i].Type; + if (argumentList.Arguments[i].Expression is not NullReferenceExpression) + continue; + if (!expectedType.IsAnonymousType() || NewAnonymousTypeInstance(expectedType) is not NewObj newObj) + continue; + var nullLiteral = argumentList.Arguments[i]; + argumentList.Arguments[i] = new ConditionalExpression(new PrimitiveExpression(true), + nullLiteral.Expression.Detach(), expressionBuilder.Translate(newObj, expectedType)) + .WithILInstruction(nullLiteral.ILInstructions) + .WithRR(new ResolveResult(expectedType)); + anyArgumentReplaced = true; + } + return anyArgumentReplaced; + } + + /// + /// Builds a 'newobj' instruction creating an instance of the anonymous type + /// with default property values; translating it yields + /// object-initializer syntax, the only way to name the type in source code. Returns null + /// if a property type involves an anonymous type other than by direct nesting (e.g. an + /// array of anonymous type), because its default value expression would have to name it. + /// + private NewObj? NewAnonymousTypeInstance(IType type) + { + var newObj = new NewObj(type.GetConstructors().Single()); + foreach (var parameter in newObj.Method.Parameters) + { + ILInstruction? argument = parameter.Type.IsAnonymousType() + ? NewAnonymousTypeInstance(parameter.Type) + : parameter.Type.ContainsAnonymousType() ? null : new DefaultValue(parameter.Type); + if (argument == null) + return null; + newObj.Arguments.Add(argument); + } + return newObj; + } + private void CastArguments(IList arguments, IList expectedParameters) { for (int i = 0; i < arguments.Count; i++)