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++)