Files
icsharpcode-ILSpy/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
T
dualfroz 8c8b8caad8 Fix #3180: Spell out type arguments of a null-conditional extension call
When the extension method call cannot be written without type arguments
(another extension method with the same name and a different arity is
found first), the call falls back to the static form. For 'a?.M()' that
fallback produced 'Enumerable.Count(s?)', which is not valid C#. Writing
the type arguments excludes the extension methods of a different arity,
so for a null-conditional target the type arguments are tried before
giving up.
2026-10-03 11:46:13 +02:00

1962 lines
79 KiB
C#

// Copyright (c) 2014 Daniel Grunwald
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using ICSharpCode.Decompiler.CSharp.Resolver;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.Semantics;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation;
using ICSharpCode.Decompiler.Util;
#nullable enable
namespace ICSharpCode.Decompiler.CSharp
{
internal struct ExpectedTargetDetails
{
public OpCode CallOpCode;
public bool NeedsBoxingConversion;
}
internal struct ArgumentList
{
public TranslatedExpression[] Arguments;
public IParameter[] ExpectedParameters;
public string[] ParameterNames;
public string[]? ArgumentNames;
public int FirstOptionalArgumentIndex;
public BitSet IsPrimitiveValue;
public IReadOnlyList<int>? ArgumentToParameterMap;
public bool AddNamesToPrimitiveValues;
public bool UseImplicitlyTypedOut;
public bool IsExpandedForm;
public int Length => Arguments.Length;
public int GetActualArgumentCount()
{
if (FirstOptionalArgumentIndex < 0)
return Arguments.Length;
Debug.Assert(FirstOptionalArgumentIndex <= Arguments.Length);
return FirstOptionalArgumentIndex;
}
/// <summary>
/// The name to write each argument with, indexed like <see cref="Arguments"/>; null where
/// every argument is written positionally.
/// </summary>
public string[]? GetArgumentNames()
{
string[]? argumentNames = ArgumentNames;
if (AddNamesToPrimitiveValues && IsPrimitiveValue.Any() && !IsExpandedForm
&& !ParameterNames.Any(string.IsNullOrEmpty))
{
if (argumentNames == null)
{
argumentNames = new string[Arguments.Length];
}
for (int i = 0; i < Arguments.Length; i++)
{
if (IsPrimitiveValue[i] && argumentNames[i] == null)
{
argumentNames[i] = ParameterNames[i];
}
}
}
// The names cover the full parameter list and have to stop where the arguments do.
int argumentCount = GetActualArgumentCount();
if (argumentNames != null && argumentNames.Length > argumentCount)
{
var writtenNames = new string[argumentCount];
Array.Copy(argumentNames, writtenNames, argumentCount);
argumentNames = writtenNames;
}
return argumentNames;
}
public IList<ResolveResult> GetArgumentResolveResults(int skipCount = 0)
{
var expectedParameters = ExpectedParameters;
var useImplicitlyTypedOut = UseImplicitlyTypedOut;
return Arguments
.SelectWithIndex(GetResolveResult)
.Take(GetActualArgumentCount())
.Skip(skipCount)
.ToArray();
ResolveResult GetResolveResult(int index, TranslatedExpression expression)
{
var param = expectedParameters[index];
if (useImplicitlyTypedOut && param.ReferenceKind == ReferenceKind.Out && expression.Type is ByReferenceType brt)
return new OutVarResolveResult(brt.ElementType);
return expression.ResolveResult;
}
}
public IList<ResolveResult> GetArgumentResolveResultsDirect(int skipCount = 0)
{
return Arguments
.Take(GetActualArgumentCount())
.Skip(skipCount)
.Select(a => a.ResolveResult)
.ToArray();
}
public IEnumerable<Expression> GetArgumentExpressions(int skipCount = 0)
{
var argumentNames = GetArgumentNames();
int argumentCount = GetActualArgumentCount();
var useImplicitlyTypedOut = UseImplicitlyTypedOut;
if (argumentNames == null)
{
return Arguments.Take(argumentCount).Skip(skipCount).Select(arg => AddAnnotations(arg.Expression));
}
else
{
// Zip stops at the shorter sequence, so names that ran short would silently drop
// the arguments past their end instead of leaving them unnamed.
Debug.Assert(argumentNames.Length == argumentCount);
return Arguments.Take(argumentCount).Skip(skipCount).Zip(argumentNames.Skip(skipCount),
(arg, name) => {
if (name == null)
return AddAnnotations(arg.Expression);
else
return new NamedArgumentExpression(name, AddAnnotations(arg.Expression));
});
}
Expression AddAnnotations(Expression expression)
{
if (!useImplicitlyTypedOut)
return expression;
if (expression.GetResolveResult() is ByReferenceResolveResult { ReferenceKind: ReferenceKind.Out } brrr)
{
expression.AddAnnotation(UseImplicitlyTypedOutAnnotation.Instance);
}
return expression;
}
}
public bool CanInferAnonymousTypePropertyNamesFromArguments()
{
for (int i = 0; i < Arguments.Length; i++)
{
string? inferredName;
switch (Arguments[i].Expression)
{
case IdentifierExpression identifier:
inferredName = identifier.Identifier;
break;
case MemberReferenceExpression member:
inferredName = member.MemberName;
break;
default:
inferredName = null;
break;
}
if (inferredName != ExpectedParameters[i].Name)
{
return false;
}
}
return true;
}
[Conditional("DEBUG")]
public void CheckNoNamedOrOptionalArguments()
{
Debug.Assert(ArgumentToParameterMap == null && ArgumentNames == null && FirstOptionalArgumentIndex < 0);
}
}
struct CallBuilder
{
readonly DecompilerSettings settings;
readonly ExpressionBuilder expressionBuilder;
readonly CSharpResolver resolver;
readonly IDecompilerTypeSystem typeSystem;
public CallBuilder(ExpressionBuilder expressionBuilder, IDecompilerTypeSystem typeSystem, DecompilerSettings settings)
{
this.expressionBuilder = expressionBuilder;
this.resolver = expressionBuilder.resolver;
this.settings = settings;
this.typeSystem = typeSystem;
}
public TranslatedExpression Build(CallInstruction inst, IType? typeHint = null)
{
if (inst is NewObj newobj && IL.Transforms.DelegateConstruction.MatchDelegateConstruction(newobj, out _, out _, out _))
{
return HandleDelegateConstruction(newobj);
}
if (settings.TupleTypes && TupleTransform.MatchTupleConstruction(inst as NewObj, out var tupleElements) && tupleElements.Length >= 2)
{
var elementTypes = TupleType.GetTupleElementTypes(inst.Method.DeclaringType);
var elementNames = typeHint is TupleType tt ? tt.ElementNames : default;
Debug.Assert(!elementTypes.IsDefault, "MatchTupleConstruction should not succeed unless we got a valid tuple type.");
Debug.Assert(elementTypes.Length == tupleElements.Length);
var tuple = new TupleExpression();
var elementRRs = new List<ResolveResult>();
foreach (var (index, element, elementType) in tupleElements.ZipWithIndex(elementTypes))
{
var translatedElement = expressionBuilder.Translate(element, elementType)
.ConvertTo(elementType, expressionBuilder, allowImplicitConversion: true);
if (elementNames.IsDefaultOrEmpty || elementNames.ElementAtOrDefault(index) is not string { Length: > 0 } name)
{
tuple.Elements.Add(translatedElement.Expression);
}
else
{
tuple.Elements.Add(new NamedArgumentExpression(name, translatedElement.Expression));
}
elementRRs.Add(translatedElement.ResolveResult);
}
return tuple.WithRR(new TupleResolveResult(
expressionBuilder.compilation,
elementRRs.ToImmutableArray(),
elementNames,
valueTupleAssembly: inst.Method.DeclaringType.GetDefinition()?.ParentModule
)).WithILInstruction(inst);
}
if (settings.StringConcat && IsSpanBasedStringConcat(inst, out var operands))
{
return BuildStringConcat(inst.Method, operands).WithILInstruction(inst);
}
var result = Build(inst.OpCode, inst.Method, inst.Arguments, constrainedTo: inst.ConstrainedTo)
.WithILInstruction(inst);
if (inst.IsTail)
{
// Surface the IL 'tail.' prefix as an inline marker, e.g. '/*tail.*/Callee(x)'.
// F# emits tail calls pervasively, and the prefix is otherwise dropped entirely.
result.Expression.AddLeadingTrivia(new Comment("tail.", CommentType.MultiLine));
}
return result;
}
private ExpressionWithResolveResult BuildStringConcat(IMethod method, List<(ILInstruction Instruction, KnownTypeCode TypeCode)> operands)
{
IType type = typeSystem.FindType(operands[0].TypeCode);
ExpressionWithResolveResult result = expressionBuilder.Translate(operands[0].Instruction, type).ConvertTo(type, expressionBuilder);
var rr = new MemberResolveResult(null, method);
for (int i = 1; i < operands.Count; i++)
{
type = typeSystem.FindType(operands[i].TypeCode);
var expr = expressionBuilder.Translate(operands[i].Instruction, type).ConvertTo(type, expressionBuilder);
result = new BinaryOperatorExpression(result.Expression, BinaryOperatorType.Add, expr).WithRR(rr);
}
return result;
}
static bool IsSpanBasedStringConcat(CallInstruction call, [NotNullWhen(true)] out List<(ILInstruction, KnownTypeCode)>? operands)
{
operands = null;
if (!IsSpanBasedStringConcat(call.Method))
{
return false;
}
int? firstStringArgumentIndex = null;
operands = new();
foreach (var arg in call.Arguments)
{
if (arg is Call opImplicit && IsStringToReadOnlySpanCharImplicitConversion(opImplicit.Method))
{
firstStringArgumentIndex ??= arg.ChildIndex;
operands.Add((opImplicit.Arguments.Single(), KnownTypeCode.String));
}
else if (arg is NewObj { Arguments: [AddressOf addressOf] } newObj && ILInlining.IsReadOnlySpanCharCtor(newObj.Method))
{
operands.Add((addressOf.Value, KnownTypeCode.Char));
}
else
{
return false;
}
}
return call.Arguments.Count >= 2 && firstStringArgumentIndex <= 1;
}
internal static bool IsSpanBasedStringConcat(IMethod method)
{
if (method is not { Name: "Concat", IsStatic: true })
{
return false;
}
if (!method.DeclaringType.IsKnownType(KnownTypeCode.String))
{
return false;
}
foreach (var p in method.Parameters)
{
if (!p.Type.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
return false;
if (!p.Type.TypeArguments[0].IsKnownType(KnownTypeCode.Char))
return false;
}
return true;
}
internal static bool IsStringToReadOnlySpanCharImplicitConversion(IMethod method)
{
return method.IsOperator
&& method.Name == "op_Implicit"
&& method.Parameters.Count == 1
&& method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
&& method.ReturnType.TypeArguments[0].IsKnownType(KnownTypeCode.Char)
&& method.Parameters[0].Type.IsKnownType(KnownTypeCode.String);
}
/// <summary>
/// Matches MemoryExtensions.AsSpan(string), the helper the C# 14 compiler emits for the
/// implicit span conversion from string to ReadOnlySpan&lt;char&gt;.
/// </summary>
internal static bool IsStringToReadOnlySpanCharAsSpan(IMethod method)
{
return method is { IsStatic: true, Name: "AsSpan", Parameters.Count: 1, TypeArguments.Count: 0 }
&& method.DeclaringType.FullName == "System.MemoryExtensions"
&& method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
&& method.ReturnType.TypeArguments[0].IsKnownType(KnownTypeCode.Char)
&& method.Parameters[0].Type.IsKnownType(KnownTypeCode.String);
}
/// <summary>
/// Matches ReadOnlySpan&lt;To&gt;.CastUp&lt;From&gt;(ReadOnlySpan&lt;From&gt;), the helper the
/// C# 14 compiler emits for the covariant implicit span conversion.
/// </summary>
internal static bool IsReadOnlySpanCastUp(IMethod method)
{
return method is { IsStatic: true, Name: "CastUp", Parameters.Count: 1, TypeArguments.Count: 1 }
&& method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
&& method.Parameters[0].Type.IsKnownType(KnownTypeCode.ReadOnlySpanOfT);
}
// Gets whether a call to `method` is equivalent to an implicit span conversion.
static bool IsEquivalentToSpanConversion(IMethod method)
{
if (method.DeclaringType.IsKnownType(KnownTypeCode.SpanOfT)
|| method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
if (method.IsOperator
&& method.Name == "op_Implicit")
{
return true;
}
}
return IsStringToReadOnlySpanCharAsSpan(method)
|| IsReadOnlySpanCastUp(method);
}
public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method,
IReadOnlyList<ILInstruction> callArguments,
IReadOnlyList<int>? argumentToParameterMap = null,
IType? constrainedTo = null)
{
if (method.IsExplicitInterfaceImplementation && callOpCode == OpCode.Call)
{
// Direct non-virtual call to explicit interface implementation.
// This can't really be represented in C#, but at least in the case where
// the class is sealed, we can equivalently call the interface member instead:
var interfaceMembers = method.ExplicitlyImplementedInterfaceMembers.ToList();
if (method.DeclaringTypeDefinition?.Kind == TypeKind.Class && method.DeclaringTypeDefinition.IsSealed && interfaceMembers.Count == 1)
{
method = (IMethod)interfaceMembers.Single();
callOpCode = OpCode.CallVirt;
}
}
// Used for Call, CallVirt and NewObj
var expectedTargetDetails = new ExpectedTargetDetails {
CallOpCode = callOpCode
};
ILFunction? localFunction = null;
if (method.IsLocalFunction)
{
localFunction = expressionBuilder.ResolveLocalFunction(method);
Debug.Assert(localFunction != null);
}
TranslatedExpression target;
if (callOpCode == OpCode.NewObj)
{
target = default(TranslatedExpression); // no target
}
else if (localFunction != null)
{
var ide = new IdentifierExpression(localFunction.Name!);
if (method.TypeArguments.Count > 0)
{
ide.TypeArguments.AddRange(method.TypeArguments.Select(expressionBuilder.ConvertType));
}
ide.AddAnnotation(localFunction);
target = ide.WithoutILInstruction()
.WithRR(ToMethodGroup(method, localFunction));
}
else
{
var thisArg = callArguments.FirstOrDefault();
if (thisArg is LdObjIfRef ldObjIfRef)
{
Debug.Assert(constrainedTo != null);
thisArg = ldObjIfRef.Target;
}
target = expressionBuilder.TranslateTarget(
thisArg,
nonVirtualInvocation: callOpCode == OpCode.Call || method.IsConstructor,
memberStatic: method.IsStatic,
memberDeclaringType: method.DeclaringType,
constrainedTo: constrainedTo);
if (constrainedTo == null
&& target.Expression is CastExpression cast
&& target.ResolveResult is ConversionResolveResult conversion
&& target.Type.IsKnownType(KnownTypeCode.Object)
&& conversion.Conversion.IsBoxingConversion)
{
// boxing conversion on call target?
// let's see if we can make that implicit:
target = target.UnwrapChild(cast.Expression);
// we'll need to make sure the boxing effect is preserved
expectedTargetDetails.NeedsBoxingConversion = true;
}
}
int firstParamIndex = (method.IsStatic || callOpCode == OpCode.NewObj) ? 0 : 1;
Debug.Assert(firstParamIndex == 0 || argumentToParameterMap == null
|| argumentToParameterMap[0] == -1);
var argumentList = BuildArgumentList(expectedTargetDetails, target.ResolveResult, method,
firstParamIndex, callArguments, argumentToParameterMap);
if (localFunction != null)
{
return new InvocationExpression(target, argumentList.GetArgumentExpressions())
.WithRR(new CSharpInvocationResolveResult(target.ResolveResult, method,
argumentList.GetArgumentResolveResults(), isExpandedForm: argumentList.IsExpandedForm));
}
if (method is VarArgInstanceMethod)
{
argumentList.FirstOptionalArgumentIndex = -1;
argumentList.AddNamesToPrimitiveValues = false;
argumentList.UseImplicitlyTypedOut = false;
int regularParameterCount = ((VarArgInstanceMethod)method).RegularParameterCount;
var argListArg = new UndocumentedExpression();
argListArg.UndocumentedExpressionType = UndocumentedExpressionType.ArgList;
int paramIndex = regularParameterCount;
var builder = expressionBuilder;
Debug.Assert(argumentToParameterMap == null && argumentList.ArgumentNames == null);
argListArg.Arguments.AddRange(argumentList.Arguments.Skip(regularParameterCount).Select(arg => arg.ConvertTo(argumentList.ExpectedParameters[paramIndex++].Type, builder).Expression));
var argListRR = new ResolveResult(SpecialType.ArgList);
argumentList.Arguments = argumentList.Arguments.Take(regularParameterCount)
.Concat(new[] { argListArg.WithoutILInstruction().WithRR(argListRR) }).ToArray();
method = ((VarArgInstanceMethod)method).BaseMethod;
argumentList.ExpectedParameters = method.Parameters.ToArray();
}
if (settings.Ranges)
{
if (HandleRangeConstruction(out var result, callOpCode, method, target, argumentList))
{
return result;
}
}
if (callOpCode == OpCode.NewObj)
{
return HandleConstructorCall(expectedTargetDetails, target.ResolveResult, method, argumentList);
}
if (method.Name == "Invoke" && method.DeclaringType.Kind == TypeKind.Delegate && !IsNullConditional(target))
{
return new InvocationExpression(target, argumentList.GetArgumentExpressions())
.WithRR(new CSharpInvocationResolveResult(target.ResolveResult, method,
argumentList.GetArgumentResolveResults(), isExpandedForm: argumentList.IsExpandedForm, isDelegateInvocation: true));
}
if (settings.StringInterpolation && IsInterpolatedStringCreation(method, argumentList))
{
var result = HandleStringInterpolation(method, argumentList);
if (result.Expression != null)
return result;
}
if (IsWrittenAsMemberAccess(method))
{
// Only an indexer access has an argument list to carry names or leave arguments out of.
if (method.AccessorOwner!.SymbolKind != SymbolKind.Indexer)
{
argumentList.CheckNoNamedOrOptionalArguments();
}
return HandleAccessorCall(expectedTargetDetails, method, target, argumentList);
}
if (IsDelegateEqualityComparison(method, argumentList.Arguments))
{
argumentList.CheckNoNamedOrOptionalArguments();
return HandleDelegateEqualityComparison(method, argumentList.Arguments)
.WithRR(new CSharpInvocationResolveResult(target.ResolveResult, method,
argumentList.GetArgumentResolveResults(), isExpandedForm: argumentList.IsExpandedForm));
}
if (method.IsOperator && method.Name == "op_Implicit" && argumentList.Length == 1)
{
argumentList.CheckNoNamedOrOptionalArguments();
return HandleImplicitConversion(method, argumentList.Arguments[0]);
}
if (settings.FirstClassSpanTypes && argumentList.Length == 1
&& (IsStringToReadOnlySpanCharAsSpan(method) || IsReadOnlySpanCastUp(method)))
{
// The C# 14 compiler emits these helpers for implicit span conversions; fold the
// call back into the conversion. Only safe when the conversion actually applies
// to this argument type - otherwise keep the call (e.g. AsSpan on a null literal).
var spanConv = CSharpConversions.Get(expressionBuilder.compilation)
.ImplicitConversion(argumentList.Arguments[0].Type, method.ReturnType);
if (spanConv.IsImplicitSpanConversion)
{
argumentList.CheckNoNamedOrOptionalArguments();
return HandleImplicitConversion(method, argumentList.Arguments[0]);
}
}
if (settings.InlineArrays
&& method is { DeclaringType.FullName: "<PrivateImplementationDetails>", Name: "InlineArrayAsSpan" or "InlineArrayAsReadOnlySpan" }
&& argumentList.Length == 2)
{
argumentList.CheckNoNamedOrOptionalArguments();
var arrayType = method.TypeArguments[0];
var arrayLength = arrayType.GetInlineArrayLength();
var arrayElementType = arrayType.GetInlineArrayElementType();
var argument = argumentList.Arguments[0];
var spanLengthExpr = argumentList.Arguments[1];
var targetType = method.ReturnType;
var spanType = typeSystem.FindType(KnownTypeCode.SpanOfT);
if (argument.Expression is DirectionExpression { FieldDirection: FieldDirection.In or FieldDirection.Ref, Expression: var lvalueExpr })
{
// `(TargetType)(in arg)` is invalid syntax.
// Also, `f(in arg)` is invalid when there's an implicit conversion involved.
argument = argument.UnwrapChild(lvalueExpr);
}
if (spanLengthExpr.ResolveResult.ConstantValue is int spanLength && spanLength <= arrayLength)
{
if (spanLength < arrayLength)
{
argument = new IndexerExpression(argument.Expression, new BinaryOperatorExpression {
Operator = BinaryOperatorType.Range,
Right = spanLengthExpr.Expression
}).WithRR(new ResolveResult(new ParameterizedType(spanType, arrayElementType))).WithoutILInstruction();
if (targetType.IsKnownType(KnownTypeCode.SpanOfT))
{
return argument;
}
}
return new CastExpression(expressionBuilder.ConvertType(targetType), argument.Expression)
.WithRR(new ConversionResolveResult(targetType, argument.ResolveResult, Conversion.InlineArrayConversion));
}
}
if (settings.LiftNullables && method.Name == "GetValueOrDefault"
&& method.DeclaringType.IsKnownType(KnownTypeCode.NullableOfT)
&& method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Boolean)
&& argumentList.Length == 0)
{
argumentList.CheckNoNamedOrOptionalArguments();
return new BinaryOperatorExpression(
target.Expression,
BinaryOperatorType.Equality,
new PrimitiveExpression(true))
.WithRR(new CSharpInvocationResolveResult(target.ResolveResult, method,
argumentList.GetArgumentResolveResults(), isExpandedForm: argumentList.IsExpandedForm));
}
var transform = GetRequiredTransformationsForCall(expectedTargetDetails, method, ref target,
ref argumentList, ReferenceTransformation.All, out IParameterizedMember? foundMethod);
// GetRequiredTransformationsForCall always assigns foundMethod (the resolved overload or 'method').
Debug.Assert(foundMethod != null);
// Note: after this, 'method' and 'foundMethod' may differ,
// but as far as allowed by IsAppropriateCallTarget().
// Need to update list of parameter names, because foundMethod is different and thus might use different names.
if (!method.Equals(foundMethod) && argumentList.ParameterNames.Length >= foundMethod!.Parameters.Count)
{
for (int i = 0; i < foundMethod.Parameters.Count; i++)
{
argumentList.ParameterNames[i] = foundMethod.Parameters[i].Name;
}
}
Expression targetExpr;
string methodName = method.Name;
AstNodeCollection<AstType> typeArgumentList;
if ((transform & ReferenceTransformation.NoNamedArgsForPrettiness) != 0)
{
argumentList.AddNamesToPrimitiveValues = false;
}
if ((transform & ReferenceTransformation.NoOptionalArgumentAllowed) != 0)
{
argumentList.FirstOptionalArgumentIndex = -1;
}
if (TryUseExtensionMethodSyntax(foundMethod, transform, argumentList, out var extensionTarget,
out var extensionTargetResolveResult))
{
return new InvocationExpression(extensionTarget, argumentList.GetArgumentExpressions(skipCount: 1))
.WithRR(new CSharpInvocationResolveResult(extensionTargetResolveResult, foundMethod,
argumentList.GetArgumentResolveResultsDirect(skipCount: 1),
isExtensionMethodInvocation: true, isExpandedForm: argumentList.IsExpandedForm));
}
if ((transform & ReferenceTransformation.RequireTarget) != 0)
{
targetExpr = new MemberReferenceExpression(target.Expression, methodName);
typeArgumentList = ((MemberReferenceExpression)targetExpr).TypeArguments;
// HACK : convert this.Dispose() to ((IDisposable)this).Dispose(), if Dispose is an explicitly implemented interface method.
// settings.AlwaysCastTargetsOfExplicitInterfaceImplementationCalls == true is used in Windows Forms' InitializeComponent methods.
if (method.IsExplicitInterfaceImplementation && (target.Expression is ThisReferenceExpression || settings.AlwaysCastTargetsOfExplicitInterfaceImplementationCalls))
{
var interfaceMember = method.ExplicitlyImplementedInterfaceMembers.First();
var castExpression = new CastExpression(expressionBuilder.ConvertType(interfaceMember.DeclaringType), target.Expression.Detach());
methodName = interfaceMember.Name;
targetExpr = new MemberReferenceExpression(castExpression, methodName);
typeArgumentList = ((MemberReferenceExpression)targetExpr).TypeArguments;
}
if (constrainedTo != null && targetExpr is MemberReferenceExpression { Target: CastExpression cast })
{
cast.AddTrailingTrivia(new Comment("cast due to constrained. prefix", CommentType.MultiLine));
}
}
else
{
targetExpr = new IdentifierExpression(methodName);
typeArgumentList = ((IdentifierExpression)targetExpr).TypeArguments;
}
if ((transform & ReferenceTransformation.RequireTypeArguments) != 0 && (!settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType())))
typeArgumentList.AddRange(method.TypeArguments.Select(expressionBuilder.ConvertType));
return new InvocationExpression(targetExpr, argumentList.GetArgumentExpressions())
.WithRR(new CSharpInvocationResolveResult(target.ResolveResult, foundMethod,
argumentList.GetArgumentResolveResultsDirect(), isExpandedForm: argumentList.IsExpandedForm));
}
private ExpressionWithResolveResult HandleStringInterpolation(IMethod method, ArgumentList argumentList)
{
if (!TryGetStringInterpolationTokens(argumentList, out string? format, out var tokens))
return default;
var arguments = argumentList.Arguments;
var content = new List<InterpolatedStringContent>();
bool unpackSingleElementArray = !argumentList.IsExpandedForm && argumentList.Length == 2
&& argumentList.Arguments[1].Expression is ArrayCreateExpression ace
&& ace.Initializer?.Elements.Count == 1;
void UnpackSingleElementArray(ref TranslatedExpression argument)
{
if (!unpackSingleElementArray)
return;
var arrayCreation = (ArrayCreateExpression)argumentList.Arguments[1].Expression;
var arrayCreationRR = (ArrayCreateResolveResult)argumentList.Arguments[1].ResolveResult;
var element = arrayCreation.Initializer!.Elements.First().Detach();
argument = new TranslatedExpression(element, arrayCreationRR.InitializerElements.First());
}
if (tokens.Count == 0)
{
return default;
}
foreach (var (kind, index, alignment, text) in tokens)
{
TranslatedExpression argument;
switch (kind)
{
case TokenKind.String:
content.Add(new InterpolatedStringText(text!));
break;
case TokenKind.Argument:
argument = arguments[index + 1];
UnpackSingleElementArray(ref argument);
content.Add(new Interpolation(argument));
break;
case TokenKind.ArgumentWithFormat:
argument = arguments[index + 1];
UnpackSingleElementArray(ref argument);
content.Add(new Interpolation(argument, suffix: text));
break;
case TokenKind.ArgumentWithAlignment:
argument = arguments[index + 1];
UnpackSingleElementArray(ref argument);
content.Add(new Interpolation(argument, alignment));
break;
case TokenKind.ArgumentWithAlignmentAndFormat:
argument = arguments[index + 1];
UnpackSingleElementArray(ref argument);
content.Add(new Interpolation(argument, alignment, text));
break;
}
}
var formattableStringType = expressionBuilder.compilation.FindType(KnownTypeCode.FormattableString);
var isrr = new InterpolatedStringResolveResult(expressionBuilder.compilation.FindType(KnownTypeCode.String),
format, argumentList.GetArgumentResolveResults(1).ToArray());
var expr = new InterpolatedStringExpression();
expr.Content.AddRange(content);
if (method.Name == "Format")
return expr.WithRR(isrr);
return new CastExpression(expressionBuilder.ConvertType(formattableStringType),
expr.WithRR(isrr))
.WithRR(new ConversionResolveResult(formattableStringType, isrr, Conversion.ImplicitInterpolatedStringConversion));
}
/// <summary>
/// Converts a call to an Add method to a collection initializer expression.
/// </summary>
public ExpressionWithResolveResult BuildCollectionInitializerExpression(OpCode callOpCode, IMethod method,
InitializedObjectResolveResult target, IReadOnlyList<ILInstruction> callArguments)
{
// (see ECMA-334, section 12.7.11.4):
// The collection object to which a collection initializer is applied shall be of a type that implements
// System.Collections.IEnumerable or a compile-time error occurs. For each specified element in order,
// the collection initializer invokes an Add method on the target object with the expression list of the
// element initializer as argument list, applying normal overload resolution for each invocation. Thus, the
// collection object shall contain an applicable Add method for each element initializer.
// The list of applicable methods includes all methods (as of C# 6.0 extension methods, too) named 'Add'
// that can be invoked on the target object, with the following exceptions:
// - Methods with ref or out parameters may not be used,
// - methods that have type parameters, that cannot be inferred from the parameter list may not be used,
// - vararg methods may not be used.
// - named arguments are not supported.
// However, note that params methods may be used.
// At this point, we assume that 'method' fulfills all the conditions mentioned above. We just need to make
// sure that the correct method is called by resolving any ambiguities by inserting casts, if necessary.
ExpectedTargetDetails expectedTargetDetails = new ExpectedTargetDetails { CallOpCode = callOpCode };
var unused = new IdentifierExpression("initializedObject").WithRR(target).WithoutILInstruction();
var args = callArguments.ToList();
if (method.IsExtensionMethod)
args.Insert(0, new Nop());
var argumentList = BuildArgumentList(expectedTargetDetails, target, method,
firstParamIndex: 0, args, null);
argumentList.ArgumentNames = null;
argumentList.AddNamesToPrimitiveValues = false;
argumentList.UseImplicitlyTypedOut = false;
// Collection initializer syntax has nowhere to put a target or type arguments, so
// casting the arguments is the only way left to pin the Add method down.
var transform = GetRequiredTransformationsForCall(expectedTargetDetails, method, ref unused,
ref argumentList,
ReferenceTransformation.CastArguments | ReferenceTransformation.NoOptionalArgumentAllowed
| ReferenceTransformation.NoNamedArgsForPrettiness,
out _);
Debug.Assert((transform & ~(ReferenceTransformation.NoOptionalArgumentAllowed | ReferenceTransformation.NoNamedArgsForPrettiness)) == 0);
// Calls with only one argument do not need an array initializer expression to wrap them.
// Any special cases are handled by the caller (i.e., ExpressionBuilder.TranslateObjectAndCollectionInitializer)
// Note: we intentionally ignore the firstOptionalArgumentIndex in this case.
int skipCount;
if (method.IsExtensionMethod)
{
if (argumentList.Arguments.Length == 2)
return argumentList.Arguments[1];
skipCount = 1;
}
else
{
if (argumentList.Arguments.Length == 1)
return argumentList.Arguments[0];
skipCount = 0;
}
if ((transform & ReferenceTransformation.NoOptionalArgumentAllowed) != 0)
argumentList.FirstOptionalArgumentIndex = -1;
return new ArrayInitializerExpression(argumentList.GetArgumentExpressions(skipCount))
.WithRR(new CSharpInvocationResolveResult(target, method, argumentList.GetArgumentResolveResults(skipCount).ToArray(),
isExtensionMethodInvocation: method.IsExtensionMethod, isExpandedForm: argumentList.IsExpandedForm));
}
public ExpressionWithResolveResult BuildDictionaryInitializerExpression(OpCode callOpCode, IMethod method,
InitializedObjectResolveResult target, IReadOnlyList<ILInstruction> indices, ILInstruction? value = null)
{
if (method is null)
throw new ArgumentNullException(nameof(method));
ExpectedTargetDetails expectedTargetDetails = new ExpectedTargetDetails { CallOpCode = callOpCode };
var callArguments = new List<ILInstruction>();
callArguments.Add(new LdNull());
callArguments.AddRange(indices);
callArguments.Add(value ?? new Nop());
// An index initializer is an assignment whatever the accessor looks like, even for a
// parameterized property, which has no access syntax of its own.
var argumentList = BuildArgumentList(expectedTargetDetails, target, method, 1, callArguments, null,
writtenAsAssignment: true);
var unused = new IdentifierExpression("initializedObject").WithRR(target).WithoutILInstruction();
var assignment = HandleAccessorCall(expectedTargetDetails, method, unused, argumentList);
if (((AssignmentExpression)assignment).Left is IndexerExpression indexer && indexer.Target is not null)
indexer.Target.Remove();
if (value != null)
return assignment;
return new ExpressionWithResolveResult(((AssignmentExpression)assignment).Left.Detach());
}
private static bool IsInterpolatedStringCreation(IMethod method, ArgumentList argumentList)
{
return method.IsStatic && (
(method.DeclaringType.IsKnownType(KnownTypeCode.String) && method.Name == "Format") ||
(method.Name == "Create" && method.DeclaringType.Name == "FormattableStringFactory" &&
method.DeclaringType.Namespace == "System.Runtime.CompilerServices")
)
&& argumentList.ArgumentNames == null // Argument names are not allowed
&& (
argumentList.IsExpandedForm // Must be expanded form
|| !method.Parameters.Last().IsParams // -or- not a params overload
|| (argumentList.Length == 2 && argumentList.Arguments[1].Expression is ArrayCreateExpression) // -or- an array literal
);
}
private bool TryGetStringInterpolationTokens(ArgumentList argumentList, [NotNullWhen(true)] out string? format, [NotNullWhen(true)] out List<(TokenKind Kind, int Index, int Alignment, string? Format)>? tokens)
{
tokens = null;
format = null;
TranslatedExpression[] arguments = argumentList.Arguments;
if (arguments.Length == 0 || argumentList.ArgumentNames != null || argumentList.ArgumentToParameterMap != null)
return false;
if (!(arguments[(int)0].ResolveResult is ConstantResolveResult crr && crr.Type.IsKnownType((KnownTypeCode)KnownTypeCode.String)))
return false;
if (!arguments.Skip(1).All(a => !a.Expression.DescendantsAndSelf.OfType<PrimitiveExpression>().Any(p => p.Value is string)))
return false;
tokens = new List<(TokenKind Kind, int Index, int Alignment, string? Format)>();
int i = 0;
format = (string)crr.ConstantValue!;
foreach (var (kind, data) in TokenizeFormatString(format))
{
int index;
string[] arg;
switch (kind)
{
case TokenKind.Error:
return false;
case TokenKind.String:
tokens.Add((kind, -1, 0, data));
break;
case TokenKind.Argument:
if (!int.TryParse(data, out index) || index != i)
return false;
i++;
tokens.Add((kind, index, 0, null));
break;
case TokenKind.ArgumentWithFormat:
arg = data!.Split(new[] { ':' }, 2);
if (arg.Length != 2 || arg[1].Length == 0)
return false;
if (!int.TryParse(arg[0], out index) || index != i)
return false;
i++;
tokens.Add((kind, index, 0, arg[1]));
break;
case TokenKind.ArgumentWithAlignment:
arg = data!.Split(new[] { ',' }, 2);
if (arg.Length != 2 || arg[1].Length == 0)
return false;
if (!int.TryParse(arg[0], out index) || index != i)
return false;
if (!int.TryParse(arg[1], out int alignment))
return false;
i++;
tokens.Add((kind, index, alignment, null));
break;
case TokenKind.ArgumentWithAlignmentAndFormat:
arg = data!.Split(new[] { ',', ':' }, 3);
if (arg.Length != 3 || arg[1].Length == 0 || arg[2].Length == 0)
return false;
if (!int.TryParse(arg[0], out index) || index != i)
return false;
if (!int.TryParse(arg[1], out alignment))
return false;
i++;
tokens.Add((kind, index, alignment, arg[2]));
break;
default:
return false;
}
}
return i == arguments.Length - 1;
}
private enum TokenKind
{
Error,
String,
Argument,
ArgumentWithFormat,
ArgumentWithAlignment,
ArgumentWithAlignmentAndFormat,
}
private IEnumerable<(TokenKind, string?)> TokenizeFormatString(string value)
{
int pos = -1;
int Peek(int steps = 1)
{
if (pos + steps < value.Length)
return value[pos + steps];
return -1;
}
int Next()
{
int val = Peek();
pos++;
return val;
}
int next;
TokenKind kind = TokenKind.String;
StringBuilder sb = new StringBuilder();
while ((next = Next()) > -1)
{
switch ((char)next)
{
case '{':
if (Peek() == '{')
{
kind = TokenKind.String;
sb.Append("{{");
Next();
}
else
{
if (sb.Length > 0)
{
yield return (kind, sb.ToString());
}
kind = TokenKind.Argument;
sb.Clear();
}
break;
case '}':
if (kind != TokenKind.String)
{
yield return (kind, sb.ToString());
sb.Clear();
kind = TokenKind.String;
}
else if (Peek() == '}')
{
sb.Append("}}");
Next();
}
else
{
yield return (TokenKind.Error, null);
}
break;
case ':':
if (kind == TokenKind.Argument)
{
kind = TokenKind.ArgumentWithFormat;
}
else if (kind == TokenKind.ArgumentWithAlignment)
{
kind = TokenKind.ArgumentWithAlignmentAndFormat;
}
sb.Append(':');
break;
case ',':
if (kind == TokenKind.Argument)
{
kind = TokenKind.ArgumentWithAlignment;
}
sb.Append(',');
break;
default:
sb.Append((char)next);
break;
}
}
if (sb.Length > 0)
{
if (kind == TokenKind.String)
yield return (kind, sb.ToString());
else
yield return (TokenKind.Error, null);
}
}
private ArgumentList BuildArgumentList(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method,
int firstParamIndex, IReadOnlyList<ILInstruction> callArguments, IReadOnlyList<int>? argumentToParameterMap,
bool writtenAsAssignment = false)
{
ArgumentList list = new ArgumentList();
// Translate arguments to the expected parameter types
var arguments = new List<TranslatedExpression>(method.Parameters.Count);
string[]? argumentNames = null;
Debug.Assert(callArguments.Count == firstParamIndex + method.Parameters.Count);
var expectedParameters = new List<IParameter>(method.Parameters.Count); // parameters, but in argument order
bool isExpandedForm = false;
BitSet isPrimitiveValue = new BitSet(method.Parameters.Count);
// Optional arguments:
// This value has the following values:
// -2 - there are no optional arguments
// -1 - optional arguments are forbidden
// >= 0 - the index of the first argument that can be removed, because it is optional
// and is the default value of the parameter.
int firstOptionalArgumentIndex = expressionBuilder.settings.OptionalArguments ? -2 : -1;
// Only an accessor written as an access or an index initializer takes its assigned
// value out of the argument list; one written as a call passes it like any other.
bool isSetter = method.ReturnType.IsKnownType(KnownTypeCode.Void)
&& (writtenAsAssignment || IsWrittenAsMemberAccess(method));
IReadOnlyList<IParameter> namedParameters = GetNamedParameters(method);
for (int i = firstParamIndex; i < callArguments.Count; i++)
{
IParameter parameter;
if (argumentToParameterMap != null)
{
if (argumentNames == null && argumentToParameterMap[i] != i - firstParamIndex)
{
// Starting at the first argument that is out-of-place,
// assign names to that argument and all following arguments:
argumentNames = new string[method.Parameters.Count];
}
int parameterIndex = argumentToParameterMap[i];
parameter = method.Parameters[parameterIndex];
// The assigned value is past the end of the indexer's parameters.
if (argumentNames != null && parameterIndex < namedParameters.Count
&& AssignVariableNames.IsValidName(namedParameters[parameterIndex].Name))
{
argumentNames[arguments.Count] = namedParameters[parameterIndex].Name;
}
}
else
{
parameter = method.Parameters[i - firstParamIndex];
}
var arg = expressionBuilder.Translate(callArguments[i], parameter.Type);
if (IsPrimitiveValueThatShouldBeNamedArgument(arg, method, parameter))
{
isPrimitiveValue.Set(arguments.Count);
}
// The assigned value of a setter is not part of the argument list, so it does not
// end the run of optional arguments either.
if (!(isSetter && i + 1 == callArguments.Count))
{
if (IsOptionalArgument(parameter, arg))
{
if (firstOptionalArgumentIndex == -2)
firstOptionalArgumentIndex = i - firstParamIndex;
}
else if (firstOptionalArgumentIndex != -1)
{
firstOptionalArgumentIndex = -2;
}
}
// An assignment has no argument list to spread a parameter array over, and C#
// cannot declare a property whose value is one.
if (expressionBuilder.settings.ExpandParamsArguments && parameter.IsParams && !isSetter
&& i + 1 == callArguments.Count && argumentToParameterMap == null)
{
// Parameter is marked params
// If the argument is an array creation, inline all elements into the call and add missing default values.
// Otherwise handle it normally.
if (TransformParamsArgument(expectedTargetDetails, target, method, parameter,
arg, ref expectedParameters, ref arguments))
{
Debug.Assert(argumentNames == null);
firstOptionalArgumentIndex = -1;
isExpandedForm = true;
continue;
}
}
IType parameterType;
if (parameter.Type.Kind == TypeKind.Dynamic)
{
parameterType = expressionBuilder.compilation.FindType(KnownTypeCode.Object);
}
else
{
parameterType = parameter.Type;
}
arg = arg.ConvertTo(parameterType, expressionBuilder, allowImplicitConversion: arg.Type.Kind != TypeKind.Dynamic);
if (method.IsOperator)
{
// Operator calls do not survive as calls: ReplaceMethodCallsWithOperators turns
// them into operator or cast syntax, where the operand determines which operator
// is resolved, so it must keep its explicit type. Unlike the null literal, which
// still narrows the candidate set, the default literal converts to every type:
// C# rejects it as the operand of any binary operator except == and != (CS8310).
arg = arg.RestoreDefaultLiteralType(expressionBuilder);
}
if (parameter.ReferenceKind != ReferenceKind.None)
{
arg = ExpressionBuilder.ChangeDirectionExpressionTo(arg, parameter.ReferenceKind, callArguments[i] is AddressOf);
// An rvalue bound to an 'in' parameter loses its DirectionExpression above and
// is an ordinary value expression: give a span conversion the same chance to
// become implicit that by-value arguments get from the ConvertTo call above.
if (arg.Expression is not DirectionExpression
&& parameter.Type.SkipModifiers() is ByReferenceType brt
&& arg.ResolveResult is ConversionResolveResult { Conversion.IsImplicitSpanConversion: true })
{
arg = arg.ConvertTo(brt.ElementType, expressionBuilder, allowImplicitConversion: true);
}
}
arguments.Add(arg);
expectedParameters.Add(parameter);
}
list.ExpectedParameters = expectedParameters.ToArray();
list.Arguments = arguments.ToArray();
list.ParameterNames = expectedParameters.SelectArray(p => p.Name);
list.ArgumentNames = argumentNames;
list.ArgumentToParameterMap = argumentToParameterMap;
list.IsExpandedForm = isExpandedForm;
list.IsPrimitiveValue = isPrimitiveValue;
list.FirstOptionalArgumentIndex = firstOptionalArgumentIndex;
list.UseImplicitlyTypedOut = true;
list.AddNamesToPrimitiveValues = expressionBuilder.settings.NamedArguments && expressionBuilder.settings.NonTrailingNamedArguments;
return list;
}
private bool IsPrimitiveValueThatShouldBeNamedArgument(TranslatedExpression arg, IMethod method, IParameter p)
{
if (!arg.ResolveResult.IsCompileTimeConstant || method.DeclaringType.IsKnownType(KnownTypeCode.NullableOfT))
return false;
return p.Type.IsKnownType(KnownTypeCode.Boolean);
}
private bool TransformParamsArgument(ExpectedTargetDetails expectedTargetDetails, ResolveResult? targetResolveResult,
IMethod method, IParameter parameter, TranslatedExpression paramsArgument, ref List<IParameter> expectedParameters,
ref List<TranslatedExpression> arguments)
{
var expressionBuilder = this.expressionBuilder;
if (ExtractArguments(out var expandedParameters, out var expandedArguments))
{
expandedParameters.InsertRange(0, expectedParameters);
expandedArguments.InsertRange(0, arguments);
if (Disambiguator.IsUnambiguousCall(expressionBuilder, expectedTargetDetails, method, targetResolveResult, Empty<IType>.Array,
expandedArguments.SelectArray(a => a.ResolveResult), argumentNames: null, out _,
out var bestCandidateIsExpandedForm) == OverloadResolutionErrors.None && bestCandidateIsExpandedForm)
{
expectedParameters = expandedParameters;
arguments = expandedArguments.SelectList(a => new TranslatedExpression(a.Expression.Detach()));
return true;
}
}
return false;
bool ExtractArguments([NotNullWhen(true)] out List<IParameter>? parameters, [NotNullWhen(true)] out List<TranslatedExpression>? arguments)
{
parameters = null;
arguments = null;
// Every expanded argument binds to the element type of the params collection, so
// that is the type the parameters standing in for them carry. The argument itself
// may be an array of a more derived element type, because the collection is
// covariant in it. Unpack it the same way overload resolution does, and give up
// where overload resolution would give up on the expanded form as well.
IType paramsElementType;
if (parameter.Type is ArrayType { Dimensions: 1 } paramsArray)
paramsElementType = paramsArray.ElementType;
else if (parameter.Type.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
|| parameter.Type.IsKnownType(KnownTypeCode.SpanOfT)
|| parameter.Type.IsArrayInterfaceType())
paramsElementType = parameter.Type.TypeArguments[0];
else
return false;
switch (paramsArgument.ResolveResult)
{
case CSharpInvocationResolveResult { Member: IMethod method, Arguments: var args }:
// match System.Array.Empty<T>()
if (args is [] && method is { IsStatic: true, FullName: "System.Array.Empty", TypeArguments: [_] })
{
arguments = new();
parameters = new();
return true;
}
// match System.ReadOnlySpan<T>..ctor(ref readonly T)
if (paramsArgument.Expression is ObjectCreateExpression oce
&& method is {
IsConstructor: true,
Parameters: [{ ReferenceKind: ReferenceKind.RefReadOnly, Type: ByReferenceType { ElementType: var paramType } }],
DeclaringType: { TypeArguments: [var type2] } declaringType
}
&& declaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
&& paramType.Equals(type2))
{
arguments = new() { new TranslatedExpression(oce.Arguments.Single()) };
parameters = new() { new DefaultParameter(paramsElementType, string.Empty) };
return true;
}
return false;
case ArrayCreateResolveResult { SizeArguments: [{ ConstantValue: int arrayLength }] }:
arguments = new(((ArrayCreateExpression)paramsArgument.Expression).Initializer?.Elements.Select(e => new TranslatedExpression(e)) ?? []);
parameters = new List<IParameter>(arrayLength);
for (int i = 0; i < arrayLength; i++)
{
parameters.Add(new DefaultParameter(paramsElementType, string.Empty));
if (arguments.Count <= i)
arguments.Add(new TranslatedExpression(expressionBuilder.GetDefaultValueExpression(paramsElementType).WithoutILInstruction()));
}
return true;
case ConversionResolveResult { Conversion.IsImplicitSpanConversion: true, Input: ArrayCreateResolveResult { SizeArguments: [{ ConstantValue: int arrayLength }] } }:
var expr = paramsArgument.Expression is CastExpression cast ? cast.Expression : paramsArgument.Expression;
arguments = new(((ArrayCreateExpression)expr).Initializer?.Elements.Select(e => new TranslatedExpression(e)) ?? []);
parameters = new List<IParameter>(arrayLength);
for (int i = 0; i < arrayLength; i++)
{
parameters.Add(new DefaultParameter(paramsElementType, string.Empty));
if (arguments.Count <= i)
arguments.Add(new TranslatedExpression(expressionBuilder.GetDefaultValueExpression(paramsElementType).WithoutILInstruction()));
}
return true;
default:
return false;
}
}
}
internal static bool IsOptionalArgument(IParameter parameter, TranslatedExpression arg)
{
if (!parameter.IsOptional)
return false;
if (!arg.ResolveResult.IsCompileTimeConstant && arg.ResolveResult is not ConversionResolveResult { Conversion.IsNullLiteralConversion: true })
return false;
if (parameter.GetAttributes().Any(a => a.AttributeType.IsKnownType(KnownAttribute.CallerMemberName)
|| a.AttributeType.IsKnownType(KnownAttribute.CallerFilePath)
|| a.AttributeType.IsKnownType(KnownAttribute.CallerLineNumber)))
return false;
return object.Equals(parameter.GetConstantValue(), arg.ResolveResult.ConstantValue);
}
/// <summary>
/// Extension method syntax is the shortest spelling of a call to an extension method: the
/// first argument becomes the target and the rest stay arguments. It is only available
/// when the name resolves back to <paramref name="foundMethod"/> from that target, which a
/// competing instance method, another extension method in scope or an inaccessible
/// declaring type can all prevent - then the call is written as the static call it is in
/// IL and this returns false.
/// <paramref name="transform"/> must be the one the call is being written with, because
/// whether the type arguments are spelled out decides which overloads the name reaches.
/// </summary>
private bool TryUseExtensionMethodSyntax(IParameterizedMember foundMethod, ReferenceTransformation transform,
ArgumentList argumentList, [NotNullWhen(true)] out MemberReferenceExpression? memberRef,
[NotNullWhen(true)] out ResolveResult? targetResolveResult)
{
memberRef = null;
targetResolveResult = null;
// The overload the call resolves to, not the one the IL named: the two can differ in
// the type arguments inference substitutes, and the check below compares the candidate
// it finds against this one for equality.
if (foundMethod is not IMethod method)
return false;
// IsExtensionMethod is false unless settings.ExtensionMethods asked the type system
// for it, so it is the gate for the setting as well.
if (!method.IsExtensionMethod || argumentList.Length == 0)
return false;
// Without using declarations every type is named in full instead. An extension method
// has no such spelling: the namespace has to be imported for the name to be found.
if (!settings.UsingDeclarations)
return false;
// The target is the first argument, so it has to be written first and positionally.
var argumentNames = argumentList.GetArgumentNames();
if (argumentNames?[0] != null)
return false;
if (argumentList.ArgumentToParameterMap is { } map && map[0] != 0)
return false;
if (argumentList.FirstOptionalArgumentIndex == 0)
return false;
var firstArgument = argumentList.Arguments[0];
bool canWriteTypeArguments = !settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType());
bool writeTypeArguments = (transform & ReferenceTransformation.RequireTypeArguments) != 0 && canWriteTypeArguments;
IType[] typeArguments = writeTypeArguments ? method.TypeArguments.ToArray() : Empty<IType>.Array;
var directionExpression = firstArgument.Expression as DirectionExpression;
ResolveResult target = firstArgument.ResolveResult;
if (target is ConstantResolveResult { ConstantValue: null } nullLiteral)
{
// A null literal has no type of its own; the target type is the one the parameter
// gives it, which the cast below then writes out.
target = new ConversionResolveResult(method.Parameters[0].Type, nullLiteral,
Conversion.NullLiteralConversion);
}
else if (directionExpression != null)
{
if (!settings.RefExtensionMethods || directionExpression.FieldDirection == FieldDirection.Out)
return false;
target = directionExpression.Expression.GetResolveResult();
}
int actualArgumentCount = argumentList.GetActualArgumentCount();
string[]? remainingNames = argumentNames?.Take(actualArgumentCount).Skip(1).ToArray();
if (remainingNames != null && remainingNames.All(name => name == null))
remainingNames = null;
var remainingArguments = argumentList.GetArgumentResolveResults(skipCount: 1).ToArray();
if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, remainingArguments, remainingNames))
{
// 'a?.M()' has no static call form to fall back to. Spelled-out type arguments rule out
// extension methods of a different arity, which NullPropagationTransform relies on.
if (writeTypeArguments || !canWriteTypeArguments || method.TypeArguments.Count == 0
|| !IsNullConditional(firstArgument.Expression))
{
return false;
}
writeTypeArguments = true;
typeArguments = method.TypeArguments.ToArray();
if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, remainingArguments, remainingNames))
return false;
}
Expression targetExpression;
if (directionExpression != null)
{
// 'ref x.Ext()' is not a thing: the target carries the reference implicitly.
targetExpression = directionExpression.Expression.Detach();
}
else if (firstArgument.Expression is NullReferenceExpression)
{
targetExpression = new CastExpression(
expressionBuilder.ConvertType(method.Parameters[0].Type), firstArgument.Expression);
}
else
{
targetExpression = firstArgument.Expression;
}
memberRef = new MemberReferenceExpression(targetExpression, method.Name);
if (writeTypeArguments)
{
memberRef.TypeArguments.AddRange(method.TypeArguments.Select(expressionBuilder.ConvertType));
}
targetResolveResult = target;
return true;
}
private ReferenceTransformation GetRequiredTransformationsForCall(ExpectedTargetDetails expectedTargetDetails, IMethod method,
ref TranslatedExpression target, ref ArgumentList argumentList, ReferenceTransformation allowedTransforms, out IParameterizedMember? foundMethod)
{
ReferenceTransformation transform = ReferenceTransformation.None;
// initialize requireTarget flag
bool requireTarget;
if ((allowedTransforms & ReferenceTransformation.RequireTarget) != 0)
{
if (method.IsLocalFunction)
{
// A local function is never reached through a target.
requireTarget = settings.AlwaysQualifyMemberReferences
|| expressionBuilder.HidesVariableWithName(method.Name);
}
else if (method.Name == ".ctor" || method.Name == ".cctor")
{
// Always use target for base/this-ctor-call, the constructor initializer pattern depends on this
requireTarget = true;
}
else
{
requireTarget = expressionBuilder.RequiresQualifier(method, target,
nonVirtualDispatch: expectedTargetDetails.CallOpCode != OpCode.CallVirt);
}
}
else
{
// HACK: this is a special case for collection initializer calls, they do not allow a target to be
// emitted, but we still need it for overload resolution.
requireTarget = true;
}
var disambiguator = Disambiguator.ForCall(expressionBuilder, method, argumentList, target,
requireTarget, expectedTargetDetails, allowedTransforms);
// Where even the most explicit spelling stays ambiguous, the call is written as it
// stands and annotated with the method it was meant to reach.
foundMethod = disambiguator.Resolved ? (IParameterizedMember?)disambiguator.FoundMember : method;
target = disambiguator.Target;
argumentList = disambiguator.Arguments;
requireTarget = disambiguator.RequireTarget;
bool requireTypeArguments = disambiguator.RequireTypeArguments;
if ((allowedTransforms & ReferenceTransformation.RequireTarget) != 0 && requireTarget)
transform |= ReferenceTransformation.RequireTarget;
if ((allowedTransforms & ReferenceTransformation.RequireTypeArguments) != 0 && requireTypeArguments)
transform |= ReferenceTransformation.RequireTypeArguments;
if (argumentList.FirstOptionalArgumentIndex < 0)
transform |= ReferenceTransformation.NoOptionalArgumentAllowed;
if (!argumentList.AddNamesToPrimitiveValues)
transform |= ReferenceTransformation.NoNamedArgsForPrettiness;
return transform;
}
static bool IsNullConditional(Expression expr)
{
return expr is UnaryOperatorExpression uoe && uoe.Operator == UnaryOperatorType.NullConditional;
}
private bool IsDelegateEqualityComparison(IMethod method, IList<TranslatedExpression> arguments)
{
// Comparison on a delegate type is a C# builtin operator
// that compiles down to a Delegate.op_Equality call.
// We handle this as a special case to avoid inserting a cast to System.Delegate.
return method.IsOperator
&& method.DeclaringType.IsKnownType(KnownTypeCode.Delegate)
&& (method.Name == "op_Equality" || method.Name == "op_Inequality")
&& arguments.Count == 2
&& arguments[0].Type.Kind == TypeKind.Delegate
&& arguments[1].Type.Equals(arguments[0].Type);
}
private Expression HandleDelegateEqualityComparison(IMethod method, IList<TranslatedExpression> arguments)
{
return new BinaryOperatorExpression(
arguments[0],
method.Name == "op_Equality" ? BinaryOperatorType.Equality : BinaryOperatorType.InEquality,
arguments[1]
);
}
private ExpressionWithResolveResult HandleImplicitConversion(IMethod method, TranslatedExpression argument)
{
var conversions = CSharpConversions.Get(expressionBuilder.compilation);
IType targetType = method.ReturnType;
var conv = conversions.ImplicitConversion(argument.Type, targetType);
// The compiler emits an implicit span conversion as a call to one of the span types'
// own members, so such a call is the conversion and folding it back is exact. Any
// other method reaching this point is a user-defined conversion operator, which only
// the user-defined conversion resolving to that very operator may be folded into.
bool directlyConvertible = conv.IsValid
&& (conv.IsUserDefined
? conv.Method.Equals(method, NormalizeTypeVisitor.TypeErasure)
: conv.IsImplicitSpanConversion && IsEquivalentToSpanConversion(method));
if (!directlyConvertible)
{
// implicit conversion to targetType isn't directly possible, so first insert a cast to the argument type
argument = argument.ConvertTo(method.Parameters[0].Type, expressionBuilder);
conv = conversions.ImplicitConversion(argument.Type, targetType);
}
if (argument.Expression is DirectionExpression { FieldDirection: FieldDirection.In, Expression: var lvalueExpr })
{
// `(TargetType)(in arg)` is invalid syntax.
// Also, `f(in arg)` is invalid when there's an implicit conversion involved.
argument = argument.UnwrapChild(lvalueExpr);
}
return new CastExpression(expressionBuilder.ConvertType(targetType), argument.Expression)
.WithRR(new ConversionResolveResult(targetType, argument.ResolveResult, conv));
}
/// <summary>The parameters a call takes its argument names from: for an indexer access the
/// indexer's, which the type system reads off the getter, not the accessor's. C# cannot
/// declare accessors that name them differently, but other languages can.</summary>
internal static IReadOnlyList<IParameter> GetNamedParameters(IMethod method)
{
return method.AccessorOwner is IProperty { IsIndexer: true } indexer
? indexer.Parameters
: method.Parameters;
}
/// <summary>Whether the accessor is written as a property or indexer access. An accessor
/// with more parameters than that syntax takes is written as a call, including the
/// assigned value.</summary>
static bool IsWrittenAsMemberAccess(IMethod method)
{
if (!method.IsAccessor)
return false;
if (method.AccessorOwner!.SymbolKind == SymbolKind.Indexer)
return true;
return method.Parameters.Count == (method.ReturnType.IsKnownType(KnownTypeCode.Void) ? 1 : 0);
}
ExpressionWithResolveResult HandleAccessorCall(ExpectedTargetDetails expectedTargetDetails, IMethod method,
TranslatedExpression target, ArgumentList argumentList)
{
// An indexer has no name of its own to write, so it can never drop its target.
bool requireTarget = method.AccessorOwner!.SymbolKind == SymbolKind.Indexer
|| expressionBuilder.RequiresQualifier(method.AccessorOwner, target,
nonVirtualDispatch: expectedTargetDetails.CallOpCode != OpCode.CallVirt);
bool isSetter = method.ReturnType.IsKnownType(KnownTypeCode.Void);
// Readability names are for arguments a call would otherwise leave unexplained; an
// access writes its index out regardless. Ambiguity is resolved by casting the target.
argumentList.AddNamesToPrimitiveValues = false;
// The accessor is probed with GetArgumentResolveResultsDirect(), which does not
// substitute OutVarResolveResult, so an implicitly typed out variable would be emitted
// against an argument list the disambiguator never validated.
argumentList.UseImplicitlyTypedOut = false;
TranslatedExpression value = default(TranslatedExpression);
if (isSetter)
{
// The assigned value is not part of the member reference, so it is removed before
// the arguments are counted, named or cast.
value = argumentList.Arguments[argumentList.Length - 1];
argumentList.Arguments = argumentList.Arguments.Take(argumentList.Length - 1).ToArray();
argumentList.ArgumentToParameterMap = argumentList.ArgumentToParameterMap
?.Take(argumentList.ArgumentToParameterMap.Count - 1).ToArray();
}
// An indexer access and an index initializer element both spell out at least one index,
// so the first one is kept whatever its default is: dropping every argument would turn
// the one into a property access and leave the other with no element to assign to.
// An index initializer is written as an element access even for a parameterized
// property, which is why the symbol kind alone does not decide this.
if (argumentList.FirstOptionalArgumentIndex == 0 && argumentList.Length > 0
&& (method.AccessorOwner.SymbolKind == SymbolKind.Indexer
|| target.ResolveResult is InitializedObjectResolveResult))
{
argumentList.FirstOptionalArgumentIndex = 1;
}
var disambiguator = Disambiguator.ForAccessor(expressionBuilder, method, argumentList, target,
requireTarget, expectedTargetDetails);
IMember? foundMember = disambiguator.Resolved ? disambiguator.FoundMember : method.AccessorOwner!;
requireTarget = disambiguator.RequireTarget;
target = disambiguator.Target;
argumentList = disambiguator.Arguments;
var rr = new MemberResolveResult(target.ResolveResult, foundMember);
// Only an indexer access writes an argument list; a property access has none, so the
// expressions are built solely on the branches that put them in the output.
bool hasArguments = argumentList.GetActualArgumentCount() != 0;
if (isSetter)
{
TranslatedExpression expr;
if (hasArguments)
{
expr = new IndexerExpression(target.ResolveResult is InitializedObjectResolveResult ? null : target.Expression,
argumentList.GetArgumentExpressions())
.WithoutILInstruction().WithRR(rr);
}
else if (requireTarget)
{
expr = new MemberReferenceExpression(target.Expression, method.AccessorOwner!.Name)
.WithoutILInstruction().WithRR(rr);
}
else
{
expr = new IdentifierExpression(method.AccessorOwner!.Name)
.WithoutILInstruction().WithRR(rr);
}
var op = AssignmentOperatorType.Assign;
if (method.AccessorOwner is IEvent parentEvent)
{
if (method.Equals(parentEvent.AddAccessor))
{
op = AssignmentOperatorType.Add;
}
if (method.Equals(parentEvent.RemoveAccessor))
{
op = AssignmentOperatorType.Subtract;
}
}
return new AssignmentExpression(expr, op, value.Expression!).WithRR(new TypeResolveResult(method.AccessorOwner!.ReturnType));
}
else
{
if (hasArguments)
{
return new IndexerExpression(target.Expression, argumentList.GetArgumentExpressions())
.WithoutILInstruction().WithRR(rr);
}
else if (requireTarget)
{
return new MemberReferenceExpression(target.Expression, method.AccessorOwner!.Name)
.WithoutILInstruction().WithRR(rr);
}
else
{
return new IdentifierExpression(method.AccessorOwner!.Name)
.WithoutILInstruction().WithRR(rr);
}
}
}
ExpressionWithResolveResult HandleConstructorCall(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method, ArgumentList argumentList)
{
if (settings.AnonymousTypes && method.DeclaringType.IsAnonymousType())
{
Debug.Assert(argumentList.ArgumentToParameterMap == null && argumentList.ArgumentNames == null && argumentList.FirstOptionalArgumentIndex < 0);
var atce = new AnonymousTypeCreateExpression();
if (argumentList.CanInferAnonymousTypePropertyNamesFromArguments())
{
atce.Initializers.AddRange(argumentList.GetArgumentExpressions());
}
else
{
for (int i = 0; i < argumentList.Length; i++)
{
atce.Initializers.Add(
new NamedExpression {
Name = argumentList.ExpectedParameters[i].Name,
Expression = argumentList.Arguments[i].ConvertTo(argumentList.ExpectedParameters[i].Type, expressionBuilder)
});
}
}
return atce.WithRR(new CSharpInvocationResolveResult(
target, method, argumentList.GetArgumentResolveResults(),
isExpandedForm: argumentList.IsExpandedForm, argumentToParameterMap: argumentList.ArgumentToParameterMap
));
}
else
{
var disambiguator = Disambiguator.ForConstructorCall(expressionBuilder, method, argumentList,
expectedTargetDetails);
argumentList = disambiguator.Arguments;
IType? returnTypeOverride = null;
if (typeSystem.MainModule.TypeSystemOptions.HasFlag(TypeSystemOptions.NativeIntegersWithoutAttribute))
{
// For DeclaringType, we don't use nint/nuint (so that DeclaringType.GetConstructors etc. works),
// but in NativeIntegersWithoutAttribute mode we must use nint/nuint for expression types,
// so that the appropriate set of conversions is used for further overload resolution.
if (method.DeclaringType.IsKnownType(KnownTypeCode.IntPtr))
returnTypeOverride = SpecialType.NInt;
else if (method.DeclaringType.IsKnownType(KnownTypeCode.UIntPtr))
returnTypeOverride = SpecialType.NUInt;
}
return new ObjectCreateExpression(
expressionBuilder.ConvertType(method.DeclaringType),
argumentList.GetArgumentExpressions()
).WithRR(new CSharpInvocationResolveResult(
target, method, argumentList.GetArgumentResolveResults().ToArray(),
isExpandedForm: argumentList.IsExpandedForm,
argumentToParameterMap: argumentList.ArgumentToParameterMap,
returnTypeOverride: returnTypeOverride
));
}
}
TranslatedExpression HandleDelegateConstruction(CallInstruction inst)
{
ILInstruction thisArg = inst.Arguments[0];
ILInstruction func = inst.Arguments[1];
IMethod method;
ExpectedTargetDetails expectedTargetDetails = default;
switch (func.OpCode)
{
case OpCode.LdFtn:
method = ((LdFtn)func).Method;
expectedTargetDetails.CallOpCode = OpCode.Call;
break;
case OpCode.LdVirtFtn:
method = ((LdVirtFtn)func).Method;
expectedTargetDetails.CallOpCode = OpCode.CallVirt;
break;
default:
throw new ArgumentException($"Unknown instruction type: {func.OpCode}");
}
if (CanUseDelegateConstruction(method, thisArg, inst.Method.DeclaringType.GetDelegateInvokeMethod()))
{
return HandleDelegateConstruction(inst.Method.DeclaringType, method, expectedTargetDetails, thisArg, inst);
}
else
{
var argumentList = BuildArgumentList(expectedTargetDetails, null, inst.Method,
0, inst.Arguments, null);
return HandleConstructorCall(new ExpectedTargetDetails { CallOpCode = OpCode.NewObj }, null, inst.Method, argumentList).WithILInstruction(inst);
}
}
private bool CanUseDelegateConstruction(IMethod targetMethod, ILInstruction thisArg, IMethod invokeMethod)
{
// Accessors cannot be directly referenced as method group in C#
// see https://github.com/icsharpcode/ILSpy/issues/1741#issuecomment-540179101
if (targetMethod.IsAccessor)
return false;
if (targetMethod.IsStatic)
{
// If the invoke method is known, we can compare the parameter counts to figure out whether the
// delegate is static or binds the first argument
if (invokeMethod != null)
{
if (invokeMethod.Parameters.Count == targetMethod.Parameters.Count)
{
return thisArg.MatchLdNull();
}
else if (targetMethod.IsExtensionMethod && invokeMethod.Parameters.Count == targetMethod.Parameters.Count - 1)
{
return true;
}
else
{
return false;
}
}
else
{
// delegate type unknown:
return thisArg.MatchLdNull() || targetMethod.IsExtensionMethod;
}
}
else
{
// targetMethod is instance method
if (invokeMethod != null && invokeMethod.Parameters.Count != targetMethod.Parameters.Count)
return false;
return true;
}
}
internal TranslatedExpression Build(LdVirtDelegate inst)
{
return HandleDelegateConstruction(inst.Type, inst.Method, new ExpectedTargetDetails { CallOpCode = OpCode.CallVirt }, inst.Argument, inst);
}
internal ExpressionWithResolveResult BuildMethodReference(IMethod method, bool isVirtual)
{
var expr = BuildDelegateReference(method, invokeMethod: null, new ExpectedTargetDetails { CallOpCode = isVirtual ? OpCode.CallVirt : OpCode.Call }, thisArg: null);
expr.Expression.RemoveAnnotations<ResolveResult>();
return expr.Expression.WithRR(new MemberResolveResult(null, method));
}
ExpressionWithResolveResult BuildDelegateReference(IMethod method, IMethod? invokeMethod, ExpectedTargetDetails expectedTargetDetails, ILInstruction? thisArg)
{
ExpressionBuilder expressionBuilder = this.expressionBuilder;
ExpressionWithResolveResult targetExpression;
(TranslatedExpression target, bool addTypeArguments, string methodName, ResolveResult result) = DisambiguateDelegateReference(method, invokeMethod, expectedTargetDetails, thisArg);
if (target.Expression != null)
{
var mre = new MemberReferenceExpression(target, methodName);
if (addTypeArguments)
{
mre.TypeArguments.AddRange(method.TypeArguments.Select(expressionBuilder.ConvertType));
}
targetExpression = mre.WithRR(result);
}
else
{
var ide = new IdentifierExpression(methodName);
if (addTypeArguments)
{
ide.TypeArguments.AddRange(method.TypeArguments.Select(expressionBuilder.ConvertType));
}
targetExpression = ide.WithRR(result);
}
return targetExpression;
}
(TranslatedExpression target, bool addTypeArguments, string methodName, ResolveResult result) DisambiguateDelegateReference(IMethod method, IMethod? invokeMethod, ExpectedTargetDetails expectedTargetDetails, ILInstruction? thisArg)
{
if (method.IsLocalFunction)
{
ILFunction? localFunction = expressionBuilder.ResolveLocalFunction(method);
Debug.Assert(localFunction != null);
return (default, addTypeArguments: true, localFunction.Name!, ToMethodGroup(method, localFunction));
}
if (method.IsExtensionMethod && method.Parameters.Count - 1 == invokeMethod?.Parameters.Count)
{
IType targetType = method.Parameters[0].Type;
if (targetType.Kind == TypeKind.ByReference && thisArg is Box thisArgBox)
{
targetType = ((ByReferenceType)targetType).ElementType;
thisArg = thisArgBox.Argument;
}
TranslatedExpression target = expressionBuilder.Translate(thisArg!, targetType);
// A null literal carries no type at all, so its cast cannot wait its turn.
var disambiguator = Disambiguator.ForMethodReference(expressionBuilder, method, targetType,
target, requireTarget: true, expectedTargetDetails, isExtensionMethodReference: true,
castTargetUpFront: thisArg!.MatchLdNull());
return (disambiguator.Target, disambiguator.RequireTypeArguments, method.Name, disambiguator.Result!);
}
else
{
// Prepare call target
IType targetType = method.DeclaringType;
if (targetType.IsReferenceType == false && thisArg is Box thisArgBox)
{
// Normal struct instance method calls (which TranslateTarget is meant for) expect a 'ref T',
// but delegate construction uses a 'box T'.
if (thisArgBox.Argument is LdObj ldobj)
{
thisArg = ldobj.Target;
}
else
{
thisArg = new AddressOf(thisArgBox.Argument, thisArgBox.Type);
}
}
TranslatedExpression target = expressionBuilder.TranslateTarget(thisArg,
nonVirtualInvocation: expectedTargetDetails.CallOpCode == OpCode.Call,
memberStatic: method.IsStatic,
memberDeclaringType: method.DeclaringType);
// check if target is required
bool requireTarget = expressionBuilder.RequiresQualifier(method, target,
nonVirtualDispatch: expectedTargetDetails.CallOpCode != OpCode.CallVirt);
var disambiguator = Disambiguator.ForMethodReference(expressionBuilder, method, targetType,
target, requireTarget, expectedTargetDetails, isExtensionMethodReference: false);
ResolveResult? result = disambiguator.Result;
if (result is MethodGroupResolveResult mgrr)
{
result = mgrr.WithChosenMethod(method);
}
// BuildDelegateReference tells a qualified reference from an unqualified one by
// whether it got a target expression at all.
return (disambiguator.RequireTarget ? disambiguator.Target : default,
disambiguator.RequireTypeArguments, method.Name, result!);
}
}
TranslatedExpression HandleDelegateConstruction(IType delegateType, IMethod method, ExpectedTargetDetails expectedTargetDetails, ILInstruction thisArg, ILInstruction inst)
{
var invokeMethod = delegateType.GetDelegateInvokeMethod();
var targetExpression = BuildDelegateReference(method, invokeMethod, expectedTargetDetails, thisArg);
var oce = new ObjectCreateExpression(expressionBuilder.ConvertType(delegateType), targetExpression)
.WithILInstruction(inst)
.WithRR(new ConversionResolveResult(
delegateType,
targetExpression.ResolveResult,
Conversion.MethodGroupConversion(method, expectedTargetDetails.CallOpCode == OpCode.CallVirt, false)));
return oce;
}
static MethodGroupResolveResult ToMethodGroup(IMethod method, ILFunction localFunction)
{
return new MethodGroupResolveResult(
null,
localFunction.Name,
new[] {
new MethodListWithDeclaringType(
method.DeclaringType,
new IParameterizedMember[] { method }
)
}, method.TypeArguments
);
}
internal TranslatedExpression CallWithNamedArgs(Block block)
{
Debug.Assert(block.Kind == BlockKind.CallWithNamedArgs);
var call = (CallInstruction)block.FinalInstruction;
var arguments = new ILInstruction[call.Arguments.Count];
var argumentToParameterMap = new int[arguments.Length];
int firstParamIndex = call.IsInstanceCall ? 1 : 0;
// Arguments from temporary variables (VariableKind.NamedArgument):
int pos = 0;
foreach (StLoc stloc in block.Instructions)
{
Debug.Assert(stloc.Variable.LoadInstructions.Single().Parent == call);
arguments[pos] = stloc.Value;
argumentToParameterMap[pos] = stloc.Variable.LoadInstructions.Single().ChildIndex - firstParamIndex;
pos++;
}
// Remaining argument:
foreach (var arg in call.Arguments)
{
if (arg.MatchLdLoc(out var v) && v.Kind == VariableKind.NamedArgument)
{
continue; // already handled in loop above
}
arguments[pos] = arg;
argumentToParameterMap[pos] = arg.ChildIndex - firstParamIndex;
pos++;
}
Debug.Assert(pos == arguments.Length);
return Build(call.OpCode, call.Method, arguments, argumentToParameterMap, call.ConstrainedTo)
.WithILInstruction(call).WithILInstruction(block);
}
private bool HandleRangeConstruction(out ExpressionWithResolveResult result, OpCode callOpCode, IMethod method, TranslatedExpression target, ArgumentList argumentList)
{
result = default;
if (argumentList.ArgumentNames != null)
{
return false; // range syntax doesn't support named arguments
}
if (method.DeclaringType.IsKnownType(KnownTypeCode.Range))
{
if (callOpCode == OpCode.NewObj && argumentList.Length == 2)
{
result = new BinaryOperatorExpression(argumentList.Arguments[0], BinaryOperatorType.Range, argumentList.Arguments[1])
.WithRR(new MemberResolveResult(null, method));
return true;
}
else if (callOpCode == OpCode.Call && method.Name == "get_All" && argumentList.Length == 0)
{
result = new BinaryOperatorExpression(null, BinaryOperatorType.Range, null)
.WithRR(new MemberResolveResult(null, method.AccessorOwner ?? method));
return true;
}
else if (callOpCode == OpCode.Call && method.Name == "StartAt" && argumentList.Length == 1)
{
result = new BinaryOperatorExpression(argumentList.Arguments[0], BinaryOperatorType.Range, null)
.WithRR(new MemberResolveResult(null, method));
return true;
}
else if (callOpCode == OpCode.Call && method.Name == "EndAt" && argumentList.Length == 1)
{
result = new BinaryOperatorExpression(null, BinaryOperatorType.Range, argumentList.Arguments[0])
.WithRR(new MemberResolveResult(null, method));
return true;
}
}
else if (callOpCode == OpCode.NewObj && method.DeclaringType.IsKnownType(KnownTypeCode.Index))
{
if (argumentList.Length != 2)
return false;
if (!(argumentList.Arguments[1].Expression is PrimitiveExpression pe && pe.Value is true))
return false;
result = new UnaryOperatorExpression(UnaryOperatorType.IndexFromEnd, argumentList.Arguments[0])
.WithRR(new MemberResolveResult(null, method));
return true;
}
else if (method is SyntheticRangeIndexAccessor rangeIndexAccessor && rangeIndexAccessor.IsSlicing)
{
// For slicing the method is called Slice()/Substring(), but we still need to output indexer notation.
// So special-case range-based slicing here.
result = new IndexerExpression(target, argumentList.Arguments.Select(a => a.Expression))
.WithRR(new MemberResolveResult(target.ResolveResult, method));
return true;
}
return false;
}
}
}