Browse Source

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
pull/3878/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
17e6a63c2b
  1. 41
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypes.cs
  2. 71
      ICSharpCode.Decompiler/CSharp/CallBuilder.cs

41
ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypes.cs

@ -133,5 +133,46 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
Renamed = v.Minor 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 t)
{
Console.WriteLine(typeof(T).FullName);
}
private static Action<T> MakeAction<T>(T template)
{
return null;
}
} }
} }

71
ICSharpCode.Decompiler/CSharp/CallBuilder.cs

@ -1201,9 +1201,22 @@ namespace ICSharpCode.Decompiler.CSharp
// if necessary. // if necessary.
if (!CanInferTypeArgumentsFromArguments(method, argumentList, expressionBuilder.typeInference)) if (!CanInferTypeArgumentsFromArguments(method, argumentList, expressionBuilder.typeInference))
{ {
requireTypeArguments = true; if (settings.AnonymousTypes
typeArguments = method.TypeArguments.ToArray(); && method.TypeArguments.Any(a => a.ContainsAnonymousType())
appliedRequireTypeArgumentsShortcut = true; && 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<IType>.Array;
}
else
{
requireTypeArguments = true;
typeArguments = method.TypeArguments.ToArray();
appliedRequireTypeArgumentsShortcut = true;
}
} }
else else
{ {
@ -1378,6 +1391,58 @@ namespace ICSharpCode.Decompiler.CSharp
return success; return success;
} }
/// <summary>
/// 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: <c>true ? null : new { A = default(int) }</c>.
/// 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.
/// </summary>
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;
}
/// <summary>
/// Builds a 'newobj' instruction creating an instance of the anonymous type
/// <paramref name="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.
/// </summary>
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<TranslatedExpression> arguments, IList<IParameter> expectedParameters) private void CastArguments(IList<TranslatedExpression> arguments, IList<IParameter> expectedParameters)
{ {
for (int i = 0; i < arguments.Count; i++) for (int i = 0; i < arguments.Count; i++)

Loading…
Cancel
Save