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.
1962 lines
79 KiB
C#
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<char>.
|
|
/// </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<To>.CastUp<From>(ReadOnlySpan<From>), 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;
|
|
}
|
|
}
|
|
}
|