|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.CodeGen;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp
{
internal partial class RefSafetyAnalysis
{
private enum EscapeLevel : uint
{
CallingMethod = CallingMethodScope,
ReturnOnly = ReturnOnlyScope,
}
/// <summary>
/// Encapsulates a symbol used in ref safety analysis. For properties and indexers this
/// captures the accessor(s) on it that were used. The particular accessor used is
/// important as it can impact ref safety analysis.
/// </summary>
private readonly struct MethodInfo
{
internal Symbol Symbol { get; }
/// <summary>
/// This is the primary <see cref="MethodSymbol" /> used in ref safety analysis.
/// </summary>
/// <remarks>
/// This will be null in error scenarios. For example when an indexer with only a set
/// method is used in a get scenario. That will lead to a non-null <see cref="MethodInfo.Symbol"/>
/// but a null value here.
/// </remarks>
internal MethodSymbol? Method { get; }
/// <summary>
/// In the case of a compound operation on non-ref return property or indexer
/// <see cref="Method"/> will represent the `get` accessor and this will
/// represent the `set` accessor.
/// </summary>
internal MethodSymbol? SetMethod { get; }
internal bool UseUpdatedEscapeRules => Method?.UseUpdatedEscapeRules == true;
internal bool ReturnsRefToRefStruct =>
Method is { RefKind: not RefKind.None, ReturnType: { } returnType } &&
returnType.IsRefLikeOrAllowsRefLikeType();
private MethodInfo(Symbol symbol, MethodSymbol? method, MethodSymbol? setMethod)
{
Symbol = symbol;
Method = method;
SetMethod = setMethod;
}
internal static MethodInfo Create(MethodSymbol method)
{
return new MethodInfo(method, method, null);
}
internal static MethodInfo Create(PropertySymbol property)
{
return new MethodInfo(
property,
property.GetOwnOrInheritedGetMethod() ?? property.GetOwnOrInheritedSetMethod(),
null);
}
internal static MethodInfo Create(PropertySymbol property, AccessorKind accessorKind) =>
accessorKind switch
{
AccessorKind.Get => new MethodInfo(property, property.GetOwnOrInheritedGetMethod(), setMethod: null),
AccessorKind.Set => new MethodInfo(property, property.GetOwnOrInheritedSetMethod(), setMethod: null),
AccessorKind.Both => new MethodInfo(property, property.GetOwnOrInheritedGetMethod(), property.GetOwnOrInheritedSetMethod()),
_ => throw ExceptionUtilities.UnexpectedValue(accessorKind),
};
internal static MethodInfo Create(BoundIndexerAccess expr) =>
Create(expr.Indexer, expr.AccessorKind);
public override string? ToString() => Method?.ToString();
}
/// <summary>
/// The destination in a method arguments must match (MAMM) check. This is
/// created primarily for ref and out arguments of a ref struct. It also applies
/// to function pointer this and arglist arguments.
/// </summary>
private readonly struct MixableDestination
{
internal BoundExpression Argument { get; }
/// <summary>
/// In the case this is the argument for a ref / out parameter this will refer
/// to the corresponding parameter. This will be null in cases like arguments
/// passed to an arglist.
/// </summary>
internal ParameterSymbol? Parameter { get; }
/// <summary>
/// This destination can only be written to by arguments that have an equal or
/// wider escape level. An destination that is <see cref="EscapeLevel.CallingMethod"/>
/// can never be written to by an argument that has a level of <see cref="EscapeLevel.ReturnOnly"/>.
/// </summary>
internal EscapeLevel EscapeLevel { get; }
internal MixableDestination(ParameterSymbol parameter, BoundExpression argument)
{
Debug.Assert(parameter.RefKind.IsWritableReference() && parameter.Type.IsRefLikeOrAllowsRefLikeType());
Debug.Assert(GetParameterValEscapeLevel(parameter).HasValue);
Argument = argument;
Parameter = parameter;
EscapeLevel = GetParameterValEscapeLevel(parameter)!.Value;
}
internal MixableDestination(BoundExpression argument, EscapeLevel escapeLevel)
{
Argument = argument;
Parameter = null;
EscapeLevel = escapeLevel;
}
internal bool IsAssignableFrom(EscapeLevel level) => EscapeLevel switch
{
EscapeLevel.CallingMethod => level == EscapeLevel.CallingMethod,
EscapeLevel.ReturnOnly => true,
_ => throw ExceptionUtilities.UnexpectedValue(EscapeLevel)
};
public override string? ToString() => (Parameter, Argument, EscapeLevel).ToString();
}
/// <summary>
/// Represents an argument being analyzed for escape analysis purposes. This represents the
/// argument as written. For example a `ref x` will only be represented by a single
/// <see cref="EscapeArgument"/>.
/// </summary>
private readonly struct EscapeArgument
{
/// <summary>
/// This will be null in cases like arglist or a function pointer receiver.
/// </summary>
internal ParameterSymbol? Parameter { get; }
internal BoundExpression Argument { get; }
internal RefKind RefKind { get; }
internal EscapeArgument(ParameterSymbol? parameter, BoundExpression argument, RefKind refKind, bool isArgList = false)
{
Debug.Assert(!isArgList || parameter is null);
Argument = argument;
Parameter = parameter;
RefKind = refKind;
}
public void Deconstruct(out ParameterSymbol? parameter, out BoundExpression argument, out RefKind refKind)
{
parameter = Parameter;
argument = Argument;
refKind = RefKind;
}
public override string? ToString() => Parameter is { } p
? p.ToString()
: Argument.ToString();
}
/// <summary>
/// Represents a value being analyzed for escape analysis purposes. This represents the value
/// as it contributes to escape analysis which means arguments can show up multiple times. For
/// example `ref x` will be represented as both a val and ref escape.
/// </summary>
private readonly struct EscapeValue
{
/// <summary>
/// This will be null in cases like arglist or a function pointer receiver.
/// </summary>
internal ParameterSymbol? Parameter { get; }
internal BoundExpression Argument { get; }
/// <summary>
/// This is _only_ useful when calculating MAMM as it dictates to what level the value
/// escaped to. That allows it to be filtered against the parameters it could possibly
/// write to.
/// </summary>
internal EscapeLevel EscapeLevel { get; }
internal bool IsRefEscape { get; }
internal EscapeValue(ParameterSymbol? parameter, BoundExpression argument, EscapeLevel escapeLevel, bool isRefEscape)
{
Argument = argument;
Parameter = parameter;
EscapeLevel = escapeLevel;
IsRefEscape = isRefEscape;
}
public void Deconstruct(out ParameterSymbol? parameter, out BoundExpression argument, out EscapeLevel escapeLevel, out bool isRefEscape)
{
parameter = Parameter;
argument = Argument;
escapeLevel = EscapeLevel;
isRefEscape = IsRefEscape;
}
public override string? ToString() => Parameter is { } p
? p.ToString()
: Argument.ToString();
}
/// <summary>
/// For the purpose of escape verification we operate with the depth of local scopes.
/// The depth is a uint, with smaller number representing shallower/wider scopes.
/// 0, 1 and 2 are special scopes -
/// 0 is the "calling method" scope that is outside of the containing method/lambda.
/// If something can escape to scope 0, it can escape to any scope in a given method through a ref parameter or return.
/// 1 is the "return-only" scope that is outside of the containing method/lambda.
/// If something can escape to scope 1, it can escape to any scope in a given method or can be returned, but it can't escape through a ref parameter.
/// 2 is the "current method" scope that is just inside the containing method/lambda.
/// If something can escape to scope 1, it can escape to any scope in a given method, but cannot be returned.
/// n + 1 corresponds to scopes immediately inside a scope of depth n.
/// Since sibling scopes do not intersect and a value cannot escape from one to another without
/// escaping to a wider scope, we can use simple depth numbering without ambiguity.
///
/// Generally these values are expressed via the following parameters:
/// - escapeFrom: the scope in which an expression is being evaluated. Usually the current local
/// scope
/// - escapeTo: the scope to which the values are being escaped to.
/// </summary>
private const uint CallingMethodScope = 0;
private const uint ReturnOnlyScope = 1;
private const uint CurrentMethodScope = 2;
}
#nullable disable
internal partial class Binder
{
// Some value kinds are semantically the same and the only distinction is how errors are reported
// for those purposes we reserve lowest 2 bits
private const int ValueKindInsignificantBits = 2;
private const BindValueKind ValueKindSignificantBitsMask = unchecked((BindValueKind)~((1 << ValueKindInsignificantBits) - 1));
/// <summary>
/// Expression capabilities and requirements.
/// </summary>
[Flags]
internal enum BindValueKind : ushort
{
///////////////////
// All expressions can be classified according to the following 4 capabilities:
//
/// <summary>
/// Expression can be an RHS of an assignment operation.
/// </summary>
/// <remarks>
/// The following are rvalues: values, variables, null literals, properties
/// and indexers with getters, events.
///
/// The following are not rvalues:
/// namespaces, types, method groups, anonymous functions.
/// </remarks>
RValue = 1 << ValueKindInsignificantBits,
/// <summary>
/// Expression can be the LHS of a simple assignment operation.
/// Example:
/// property with a setter
/// </summary>
Assignable = 2 << ValueKindInsignificantBits,
/// <summary>
/// Expression represents a location. Often referred as a "variable"
/// Examples:
/// local variable, parameter, field
/// </summary>
RefersToLocation = 4 << ValueKindInsignificantBits,
/// <summary>
/// Expression can be the LHS of a ref-assign operation.
/// Example:
/// ref local, ref parameter, out parameter, ref field
/// </summary>
RefAssignable = 8 << ValueKindInsignificantBits,
///////////////////
// The rest are just combinations of the above.
//
/// <summary>
/// Expression is the RHS of an assignment operation
/// and may be a method group.
/// Basically an RValue, but could be treated differently for the purpose of error reporting
/// </summary>
RValueOrMethodGroup = RValue + 1,
/// <summary>
/// Expression can be an LHS of a compound assignment
/// operation (such as +=).
/// </summary>
CompoundAssignment = RValue | Assignable,
/// <summary>
/// Expression can be the operand of an increment or decrement operation.
/// Same as CompoundAssignment, the distinction is really just for error reporting.
/// </summary>
IncrementDecrement = CompoundAssignment + 1,
/// <summary>
/// Expression is a r/o reference.
/// </summary>
ReadonlyRef = RefersToLocation | RValue,
/// <summary>
/// Expression can be the operand of an address-of operation (&).
/// Same as ReadonlyRef. The difference is just for error reporting.
/// </summary>
AddressOf = ReadonlyRef + 1,
/// <summary>
/// Expression is the receiver of a fixed buffer field access
/// Same as ReadonlyRef. The difference is just for error reporting.
/// </summary>
FixedReceiver = ReadonlyRef + 2,
/// <summary>
/// Expression is passed as a ref or out parameter or assigned to a byref variable.
/// </summary>
RefOrOut = RefersToLocation | RValue | Assignable,
/// <summary>
/// Expression is returned by an ordinary r/w reference.
/// Same as RefOrOut. The difference is just for error reporting.
/// </summary>
RefReturn = RefOrOut + 1,
}
private static bool RequiresRValueOnly(BindValueKind kind)
{
return (kind & ValueKindSignificantBitsMask) == BindValueKind.RValue;
}
private static bool RequiresAssignmentOnly(BindValueKind kind)
{
return (kind & ValueKindSignificantBitsMask) == BindValueKind.Assignable;
}
private static bool RequiresVariable(BindValueKind kind)
{
return !RequiresRValueOnly(kind);
}
private static bool RequiresReferenceToLocation(BindValueKind kind)
{
return (kind & BindValueKind.RefersToLocation) != 0;
}
private static bool RequiresAssignableVariable(BindValueKind kind)
{
return (kind & BindValueKind.Assignable) != 0;
}
private static bool RequiresRefAssignableVariable(BindValueKind kind)
{
return (kind & BindValueKind.RefAssignable) != 0;
}
private static bool RequiresRefOrOut(BindValueKind kind)
{
return (kind & BindValueKind.RefOrOut) == BindValueKind.RefOrOut;
}
#nullable enable
private static AccessorKind GetIndexerAccessorKind(BoundIndexerAccess indexerAccess, BindValueKind valueKind)
{
if (indexerAccess.Indexer.RefKind != RefKind.None)
{
return AccessorKind.Get;
}
return GetAccessorKind(valueKind);
}
private static AccessorKind GetAccessorKind(BindValueKind valueKind)
{
var coreValueKind = valueKind & ValueKindSignificantBitsMask;
return coreValueKind switch
{
BindValueKind.CompoundAssignment => AccessorKind.Both,
BindValueKind.Assignable => AccessorKind.Set,
_ => AccessorKind.Get,
};
}
private BoundIndexerAccess BindIndexerDefaultArgumentsAndParamsCollection(BoundIndexerAccess indexerAccess, BindValueKind valueKind, BindingDiagnosticBag diagnostics)
{
var coreValueKind = valueKind & ValueKindSignificantBitsMask;
AccessorKind accessorKind = GetIndexerAccessorKind(indexerAccess, valueKind);
var useSetAccessor = coreValueKind == BindValueKind.Assignable && indexerAccess.Indexer.RefKind != RefKind.Ref;
var accessorForDefaultArguments = useSetAccessor
? indexerAccess.Indexer.GetOwnOrInheritedSetMethod()
: indexerAccess.Indexer.GetOwnOrInheritedGetMethod();
if (accessorForDefaultArguments is not null)
{
var argumentsBuilder = ArrayBuilder<BoundExpression>.GetInstance(accessorForDefaultArguments.ParameterCount);
argumentsBuilder.AddRange(indexerAccess.Arguments);
ArrayBuilder<RefKind>? refKindsBuilderOpt;
if (!indexerAccess.ArgumentRefKindsOpt.IsDefaultOrEmpty)
{
refKindsBuilderOpt = ArrayBuilder<RefKind>.GetInstance(accessorForDefaultArguments.ParameterCount);
refKindsBuilderOpt.AddRange(indexerAccess.ArgumentRefKindsOpt);
}
else
{
refKindsBuilderOpt = null;
}
var argsToParams = indexerAccess.ArgsToParamsOpt;
// It is possible for the indexer 'value' parameter from metadata to have a default value, but the compiler will not use it.
// However, we may still use any default values from the preceding parameters.
var parameters = accessorForDefaultArguments.Parameters;
if (useSetAccessor)
{
parameters = parameters.RemoveAt(parameters.Length - 1);
}
BitVector defaultArguments = default;
Debug.Assert(parameters.Length == indexerAccess.Indexer.Parameters.Length);
ImmutableArray<string?> argumentNamesOpt = indexerAccess.ArgumentNamesOpt;
// If OriginalIndexersOpt is set, there was an overload resolution failure, and we don't want to make guesses about the default
// arguments that will end up being reflected in the SemanticModel/IOperation
if (indexerAccess.OriginalIndexersOpt.IsDefault)
{
ArrayBuilder<(string Name, Location Location)?>? namesBuilder = null;
if (!argumentNamesOpt.IsDefaultOrEmpty)
{
namesBuilder = ArrayBuilder<(string Name, Location Location)?>.GetInstance(argumentNamesOpt.Length);
foreach (var name in argumentNamesOpt)
{
if (name is null)
{
namesBuilder.Add(null);
}
else
{
namesBuilder.Add((name, NoLocation.Singleton));
}
}
}
BindDefaultArguments(indexerAccess.Syntax, parameters, argumentsBuilder, refKindsBuilderOpt, namesBuilder, ref argsToParams, out defaultArguments, indexerAccess.Expanded, enableCallerInfo: true, diagnostics);
if (namesBuilder is object)
{
argumentNamesOpt = namesBuilder.SelectAsArray(item => item?.Name);
namesBuilder.Free();
}
}
indexerAccess = indexerAccess.Update(
indexerAccess.ReceiverOpt,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerAccess.Indexer,
argumentsBuilder.ToImmutableAndFree(),
argumentNamesOpt,
refKindsBuilderOpt?.ToImmutableOrNull() ?? default,
indexerAccess.Expanded,
accessorKind,
argsToParams,
defaultArguments,
indexerAccess.Type);
refKindsBuilderOpt?.Free();
return indexerAccess;
}
return indexerAccess.Update(accessorKind);
}
#nullable disable
/// <summary>
/// Check the expression is of the required lvalue and rvalue specified by valueKind.
/// The method returns the original expression if the expression is of the required
/// type. Otherwise, an appropriate error is added to the diagnostics bag and the
/// method returns a BoundBadExpression node. The method returns the original
/// expression without generating any error if the expression has errors.
/// </summary>
private BoundExpression CheckValue(BoundExpression expr, BindValueKind valueKind, BindingDiagnosticBag diagnostics)
{
switch (expr.Kind)
{
case BoundKind.PropertyGroup:
{
expr = BindIndexedPropertyAccess((BoundPropertyGroup)expr, mustHaveAllOptionalParameters: false, diagnostics: diagnostics);
if (expr is BoundIndexerAccess indexerAccess)
{
expr = BindIndexerDefaultArgumentsAndParamsCollection(indexerAccess, valueKind, diagnostics);
}
}
break;
case BoundKind.Local:
Debug.Assert(expr.Syntax.Kind() != SyntaxKind.Argument || valueKind == BindValueKind.RefOrOut);
break;
case BoundKind.OutVariablePendingInference:
case BoundKind.OutDeconstructVarPendingInference:
Debug.Assert(valueKind == BindValueKind.RefOrOut);
return expr;
case BoundKind.DiscardExpression:
Debug.Assert(valueKind is (BindValueKind.Assignable or BindValueKind.RefOrOut or BindValueKind.RefAssignable) || diagnostics.DiagnosticBag is null || diagnostics.HasAnyResolvedErrors());
return expr;
case BoundKind.PropertyAccess:
if (!InAttributeArgument)
{
// If the property has a synthesized backing field, record the accessor kind of the property
// access for determining whether the property access can use the backing field directly.
var propertyAccess = (BoundPropertyAccess)expr;
if (HasSynthesizedBackingField(propertyAccess.PropertySymbol, out _))
{
expr = propertyAccess.Update(
propertyAccess.ReceiverOpt,
propertyAccess.InitialBindingReceiverIsSubjectToCloning,
propertyAccess.PropertySymbol,
autoPropertyAccessorKind: GetAccessorKind(valueKind),
propertyAccess.ResultKind,
propertyAccess.Type);
}
}
#if DEBUG
expr.WasPropertyBackingFieldAccessChecked = true;
#endif
break;
case BoundKind.IndexerAccess:
expr = BindIndexerDefaultArgumentsAndParamsCollection((BoundIndexerAccess)expr, valueKind, diagnostics);
break;
case BoundKind.ImplicitIndexerAccess:
{
var implicitIndexer = (BoundImplicitIndexerAccess)expr;
if (implicitIndexer.IndexerOrSliceAccess is BoundIndexerAccess indexerAccess)
{
var kind = GetIndexerAccessorKind(indexerAccess, valueKind);
expr = implicitIndexer.Update(
implicitIndexer.Receiver,
implicitIndexer.Argument,
implicitIndexer.LengthOrCountAccess,
implicitIndexer.ReceiverPlaceholder,
indexerAccess.Update(kind),
implicitIndexer.ArgumentPlaceholders,
implicitIndexer.Type);
}
}
break;
case BoundKind.UnconvertedObjectCreationExpression:
if (valueKind == BindValueKind.RValue)
{
return expr;
}
break;
case BoundKind.UnconvertedCollectionExpression:
if (valueKind == BindValueKind.RValue)
{
return expr;
}
break;
case BoundKind.PointerIndirectionOperator:
if ((valueKind & BindValueKind.RefersToLocation) == BindValueKind.RefersToLocation)
{
var pointerIndirection = (BoundPointerIndirectionOperator)expr;
expr = pointerIndirection.Update(pointerIndirection.Operand, refersToLocation: true, pointerIndirection.Type);
}
break;
case BoundKind.PointerElementAccess:
if ((valueKind & BindValueKind.RefersToLocation) == BindValueKind.RefersToLocation)
{
var elementAccess = (BoundPointerElementAccess)expr;
expr = elementAccess.Update(elementAccess.Expression, elementAccess.Index, elementAccess.Checked, refersToLocation: true, elementAccess.Type);
}
break;
}
bool hasResolutionErrors = false;
// If this a MethodGroup where an rvalue is not expected or where the caller will not explicitly handle
// (and resolve) MethodGroups (in short, cases where valueKind != BindValueKind.RValueOrMethodGroup),
// resolve the MethodGroup here to generate the appropriate errors, otherwise resolution errors (such as
// "member is inaccessible") will be dropped.
if (expr.Kind == BoundKind.MethodGroup && valueKind != BindValueKind.RValueOrMethodGroup)
{
var methodGroup = (BoundMethodGroup)expr;
CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
var resolution = this.ResolveMethodGroup(methodGroup, analyzedArguments: null, useSiteInfo: ref useSiteInfo, options: OverloadResolution.Options.None);
diagnostics.Add(expr.Syntax, useSiteInfo);
Symbol otherSymbol = null;
bool resolvedToMethodGroup = resolution.MethodGroup != null;
if (!expr.HasAnyErrors) diagnostics.AddRange(resolution.Diagnostics); // Suppress cascading.
hasResolutionErrors = resolution.HasAnyErrors;
if (hasResolutionErrors)
{
otherSymbol = resolution.OtherSymbol;
}
resolution.Free();
// It's possible the method group is not a method group at all, but simply a
// delayed lookup that resolved to a non-method member (perhaps an inaccessible
// field or property), or nothing at all. In those cases, the member should not be exposed as a
// method group, not even within a BoundBadExpression. Instead, the
// BoundBadExpression simply refers to the receiver and the resolved symbol (if any).
if (!resolvedToMethodGroup)
{
Debug.Assert(methodGroup.ResultKind != LookupResultKind.Viable);
var receiver = methodGroup.ReceiverOpt;
if ((object)otherSymbol != null && receiver?.Kind == BoundKind.TypeOrValueExpression)
{
// Since we're not accessing a method, this can't be a Color Color case, so TypeOrValueExpression should not have been used.
// CAVEAT: otherSymbol could be invalid in some way (e.g. inaccessible), in which case we would have fallen back on a
// method group lookup (to allow for extension methods), which would have required a TypeOrValueExpression.
Debug.Assert(methodGroup.LookupError != null);
// Since we have a concrete member in hand, we can resolve the receiver.
var typeOrValue = (BoundTypeOrValueExpression)receiver;
receiver = otherSymbol.RequiresInstanceReceiver()
? typeOrValue.Data.ValueExpression
: null; // no receiver required
}
return new BoundBadExpression(
expr.Syntax,
methodGroup.ResultKind,
(object)otherSymbol == null ? ImmutableArray<Symbol>.Empty : ImmutableArray.Create(otherSymbol),
receiver == null ? ImmutableArray<BoundExpression>.Empty : ImmutableArray.Create(receiver),
GetNonMethodMemberType(otherSymbol));
}
}
if (!hasResolutionErrors && CheckValueKind(expr.Syntax, expr, valueKind, checkingReceiver: false, diagnostics: diagnostics) ||
expr.HasAnyErrors && valueKind == BindValueKind.RValueOrMethodGroup)
{
return expr;
}
var resultKind = (valueKind == BindValueKind.RValue || valueKind == BindValueKind.RValueOrMethodGroup) ?
LookupResultKind.NotAValue :
LookupResultKind.NotAVariable;
return ToBadExpression(expr, resultKind);
}
internal static bool IsTypeOrValueExpression(BoundExpression expression)
{
switch (expression?.Kind)
{
case BoundKind.TypeOrValueExpression:
case BoundKind.QueryClause when ((BoundQueryClause)expression).Value.Kind == BoundKind.TypeOrValueExpression:
return true;
default:
return false;
}
}
/// <summary>
/// The purpose of this method is to determine if the expression satisfies desired capabilities.
/// If it is not then this code gives an appropriate error message.
///
/// To determine the appropriate error message we need to know two things:
///
/// (1) What capabilities we need - increment it, assign, return as a readonly reference, . . . ?
///
/// (2) Are we trying to determine if the left hand side of a dot is a variable in order
/// to determine if the field or property on the right hand side of a dot is assignable?
///
/// (3) The syntax of the expression that started the analysis. (for error reporting purposes).
/// </summary>
internal bool CheckValueKind(SyntaxNode node, BoundExpression expr, BindValueKind valueKind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
Debug.Assert(!checkingReceiver || expr.Type.IsValueType || expr.Type.IsTypeParameter());
if (expr.HasAnyErrors)
{
return false;
}
switch (expr.Kind)
{
// we need to handle properties and event in a special way even in an RValue case because of getters
case BoundKind.PropertyAccess:
case BoundKind.IndexerAccess:
case BoundKind.ImplicitIndexerAccess when ((BoundImplicitIndexerAccess)expr).IndexerOrSliceAccess.Kind == BoundKind.IndexerAccess:
return CheckPropertyValueKind(node, expr, valueKind, checkingReceiver, diagnostics);
case BoundKind.EventAccess:
return CheckEventValueKind((BoundEventAccess)expr, valueKind, diagnostics);
}
// easy out for a very common RValue case.
if (RequiresRValueOnly(valueKind))
{
return CheckNotNamespaceOrType(expr, diagnostics);
}
// constants/literals are strictly RValues
// void is not even an RValue
if ((expr.ConstantValueOpt != null) || (expr.Type.GetSpecialTypeSafe() == SpecialType.System_Void))
{
Error(diagnostics, GetStandardLvalueError(valueKind), node);
return false;
}
switch (expr.Kind)
{
case BoundKind.NamespaceExpression:
var ns = (BoundNamespaceExpression)expr;
Error(diagnostics, ErrorCode.ERR_BadSKknown, node, ns.NamespaceSymbol, MessageID.IDS_SK_NAMESPACE.Localize(), MessageID.IDS_SK_VARIABLE.Localize());
return false;
case BoundKind.TypeExpression:
var type = (BoundTypeExpression)expr;
Error(diagnostics, ErrorCode.ERR_BadSKknown, node, type.Type, MessageID.IDS_SK_TYPE.Localize(), MessageID.IDS_SK_VARIABLE.Localize());
return false;
case BoundKind.Lambda:
case BoundKind.UnboundLambda:
// lambdas can only be used as RValues
Error(diagnostics, GetStandardLvalueError(valueKind), node);
return false;
case BoundKind.UnconvertedAddressOfOperator:
var unconvertedAddressOf = (BoundUnconvertedAddressOfOperator)expr;
Error(diagnostics, GetMethodGroupOrFunctionPointerLvalueError(valueKind), node, unconvertedAddressOf.Operand.Name, MessageID.IDS_AddressOfMethodGroup.Localize());
return false;
case BoundKind.MethodGroup when valueKind == BindValueKind.AddressOf:
// If the addressof operator is used not as an rvalue, that will get flagged when CheckValue
// is called on the parent BoundUnconvertedAddressOf node.
return true;
case BoundKind.MethodGroup:
// method groups can only be used as RValues except when taking the address of one
var methodGroup = (BoundMethodGroup)expr;
Error(diagnostics, GetMethodGroupOrFunctionPointerLvalueError(valueKind), node, methodGroup.Name, MessageID.IDS_MethodGroup.Localize());
return false;
case BoundKind.RangeVariable:
{
// range variables can only be used as RValues
var queryref = (BoundRangeVariable)expr;
var errorCode = GetRangeLvalueError(valueKind);
if (errorCode is ErrorCode.ERR_InvalidAddrOp or ErrorCode.ERR_RefLocalOrParamExpected)
{
Error(diagnostics, errorCode, node);
}
else
{
Error(diagnostics, errorCode, node, queryref.RangeVariableSymbol.Name);
}
return false;
}
case BoundKind.Conversion:
var conversion = (BoundConversion)expr;
// conversions are strict RValues, but unboxing has a specific error
if (conversion.ConversionKind == ConversionKind.Unboxing)
{
Error(diagnostics, ErrorCode.ERR_UnboxNotLValue, node);
return false;
}
break;
// array access is readwrite variable if the indexing expression is not System.Range
case BoundKind.ArrayAccess:
return checkArrayAccessValueKind(node, valueKind, ((BoundArrayAccess)expr).Indices, diagnostics);
// pointer dereferencing is a readwrite variable
case BoundKind.PointerIndirectionOperator:
// The undocumented __refvalue(tr, T) expression results in a variable of type T.
case BoundKind.RefValueOperator:
// dynamic expressions are readwrite, and can even be passed by ref (which is implemented via a temp)
case BoundKind.DynamicMemberAccess:
case BoundKind.DynamicIndexerAccess:
case BoundKind.DynamicObjectInitializerMember:
{
if (RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
// These are readwrite variables
return true;
}
case BoundKind.PointerElementAccess:
{
if (RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
var receiver = ((BoundPointerElementAccess)expr).Expression;
if (receiver is BoundFieldAccess fieldAccess && fieldAccess.FieldSymbol.IsFixedSizeBuffer)
{
return CheckValueKind(node, fieldAccess.ReceiverOpt, valueKind, checkingReceiver: true, diagnostics);
}
return true;
}
case BoundKind.Parameter:
var parameter = (BoundParameter)expr;
return CheckParameterValueKind(node, parameter, valueKind, checkingReceiver, diagnostics);
case BoundKind.Local:
var local = (BoundLocal)expr;
return CheckLocalValueKind(node, local, valueKind, checkingReceiver, diagnostics);
case BoundKind.ThisReference:
// `this` is never ref assignable
if (RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
// We will already have given an error for "this" used outside of a constructor,
// instance method, or instance accessor. Assume that "this" is a variable if it is in a struct.
// SPEC: when this is used in a primary-expression within an instance constructor of a struct,
// SPEC: it is classified as a variable.
// SPEC: When this is used in a primary-expression within an instance method or instance accessor
// SPEC: of a struct, it is classified as a variable.
// Note: RValueOnly is checked at the beginning of this method. Since we are here we need more than readable.
// "this" is readonly in members marked "readonly" and in members of readonly structs, unless we are in a constructor.
var isValueType = ((BoundThisReference)expr).Type.IsValueType;
if (!isValueType || (RequiresAssignableVariable(valueKind) && (this.ContainingMemberOrLambda as MethodSymbol)?.IsEffectivelyReadOnly == true))
{
ReportThisLvalueError(node, valueKind, isValueType, isPrimaryConstructorParameter: false, diagnostics);
return false;
}
return true;
case BoundKind.ImplicitReceiver:
case BoundKind.ObjectOrCollectionValuePlaceholder:
Debug.Assert(!RequiresRefAssignableVariable(valueKind));
return true;
case BoundKind.Call:
var call = (BoundCall)expr;
return CheckMethodReturnValueKind(call.Method, call.Syntax, node, valueKind, checkingReceiver, diagnostics);
case BoundKind.FunctionPointerInvocation:
return CheckMethodReturnValueKind(((BoundFunctionPointerInvocation)expr).FunctionPointer.Signature,
expr.Syntax,
node,
valueKind,
checkingReceiver,
diagnostics);
case BoundKind.ImplicitIndexerAccess:
var implicitIndexer = (BoundImplicitIndexerAccess)expr;
switch (implicitIndexer.IndexerOrSliceAccess)
{
case BoundArrayAccess arrayAccess:
return checkArrayAccessValueKind(node, valueKind, arrayAccess.Indices, diagnostics);
case BoundCall sliceAccess:
return CheckMethodReturnValueKind(sliceAccess.Method, sliceAccess.Syntax, node, valueKind, checkingReceiver, diagnostics);
default:
throw ExceptionUtilities.UnexpectedValue(implicitIndexer.IndexerOrSliceAccess.Kind);
}
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)expr;
if (elementAccess.IsValue || elementAccess.GetItemOrSliceHelper is WellKnownMember.System_Span_T__Slice_Int_Int or WellKnownMember.System_ReadOnlySpan_T__Slice_Int_Int)
{
// Strict RValue
break;
}
var getItemOrSliceHelper = (MethodSymbol)Compilation.GetWellKnownTypeMember(elementAccess.GetItemOrSliceHelper);
if (getItemOrSliceHelper is null)
{
return true;
}
getItemOrSliceHelper = getItemOrSliceHelper.AsMember(getItemOrSliceHelper.ContainingType.Construct(ImmutableArray.Create(elementAccess.Expression.Type.TryGetInlineArrayElementField().TypeWithAnnotations)));
return CheckMethodReturnValueKind(getItemOrSliceHelper, elementAccess.Syntax, node, valueKind, checkingReceiver, diagnostics);
}
case BoundKind.ImplicitIndexerReceiverPlaceholder:
break;
case BoundKind.DeconstructValuePlaceholder:
break;
case BoundKind.ConditionalOperator:
if (RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
var conditional = (BoundConditionalOperator)expr;
// byref conditional defers to its operands
if (conditional.IsRef &&
(CheckValueKind(conditional.Consequence.Syntax, conditional.Consequence, valueKind, checkingReceiver: false, diagnostics: diagnostics) &
CheckValueKind(conditional.Alternative.Syntax, conditional.Alternative, valueKind, checkingReceiver: false, diagnostics: diagnostics)))
{
return true;
}
// report standard lvalue error
break;
case BoundKind.FieldAccess:
{
var fieldAccess = (BoundFieldAccess)expr;
return CheckFieldValueKind(node, fieldAccess, valueKind, checkingReceiver, diagnostics);
}
case BoundKind.AssignmentOperator:
// Cannot ref-assign to a ref assignment.
if (RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
var assignment = (BoundAssignmentOperator)expr;
return CheckSimpleAssignmentValueKind(node, assignment, valueKind, diagnostics);
case BoundKind.ValuePlaceholder:
// Strict RValue
break;
default:
RoslynDebug.Assert(expr is not BoundValuePlaceholderBase, $"Placeholder kind {expr.Kind} should be explicitly handled");
break;
}
// At this point we should have covered all the possible cases for anything that is not a strict RValue.
Error(diagnostics, GetStandardLvalueError(valueKind), node);
return false;
bool checkArrayAccessValueKind(SyntaxNode node, BindValueKind valueKind, ImmutableArray<BoundExpression> indices, BindingDiagnosticBag diagnostics)
{
if (RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
if (indices.Length == 1 &&
TypeSymbol.Equals(
indices[0].Type,
Compilation.GetWellKnownType(WellKnownType.System_Range),
TypeCompareKind.ConsiderEverything))
{
// Range indexer is an rvalue
Error(diagnostics, GetStandardLvalueError(valueKind), node);
return false;
}
return true;
}
}
private static void ReportThisLvalueError(SyntaxNode node, BindValueKind valueKind, bool isValueType, bool isPrimaryConstructorParameter, BindingDiagnosticBag diagnostics)
{
var errorCode = GetThisLvalueError(valueKind, isValueType, isPrimaryConstructorParameter);
if (errorCode is ErrorCode.ERR_InvalidAddrOp or ErrorCode.ERR_IncrementLvalueExpected or ErrorCode.ERR_RefReturnThis or ErrorCode.ERR_RefLocalOrParamExpected or ErrorCode.ERR_RefLvalueExpected)
{
Error(diagnostics, errorCode, node);
}
else
{
Error(diagnostics, errorCode, node, node);
}
}
private static bool CheckNotNamespaceOrType(BoundExpression expr, BindingDiagnosticBag diagnostics)
{
switch (expr.Kind)
{
case BoundKind.NamespaceExpression:
Error(diagnostics, ErrorCode.ERR_BadSKknown, expr.Syntax, ((BoundNamespaceExpression)expr).NamespaceSymbol, MessageID.IDS_SK_NAMESPACE.Localize(), MessageID.IDS_SK_VARIABLE.Localize());
return false;
case BoundKind.TypeExpression:
Error(diagnostics, ErrorCode.ERR_BadSKunknown, expr.Syntax, expr.Type, MessageID.IDS_SK_TYPE.Localize());
return false;
default:
return true;
}
}
private void CheckAddressOfInAsyncOrIteratorMethod(SyntaxNode node, BindValueKind valueKind, BindingDiagnosticBag diagnostics)
{
if (valueKind == BindValueKind.AddressOf)
{
if (this.IsInAsyncMethod())
{
Error(diagnostics, ErrorCode.WRN_AddressOfInAsync, node);
}
else if (this.IsDirectlyInIterator && Compilation.IsFeatureEnabled(MessageID.IDS_FeatureRefUnsafeInIteratorAsync))
{
Error(diagnostics, ErrorCode.ERR_AddressOfInIterator, node);
}
}
}
private bool CheckLocalValueKind(SyntaxNode node, BoundLocal local, BindValueKind valueKind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
CheckAddressOfInAsyncOrIteratorMethod(node, valueKind, diagnostics);
// Local constants are never variables. Local variables are sometimes
// not to be treated as variables, if they are fixed, declared in a using,
// or declared in a foreach.
LocalSymbol localSymbol = local.LocalSymbol;
if (RequiresAssignableVariable(valueKind))
{
if (this.LockedOrDisposedVariables.Contains(localSymbol))
{
diagnostics.Add(ErrorCode.WRN_AssignmentToLockOrDispose, local.Syntax.Location, localSymbol);
}
// IsWritable means the variable is writable. If this is a ref variable, IsWritable
// does not imply anything about the storage location
if (localSymbol.RefKind == RefKind.RefReadOnly ||
(localSymbol.RefKind == RefKind.None && !localSymbol.IsWritableVariable))
{
ReportReadonlyLocalError(node, localSymbol, valueKind, checkingReceiver, diagnostics);
return false;
}
}
else if (RequiresRefAssignableVariable(valueKind))
{
if (localSymbol.RefKind == RefKind.None)
{
diagnostics.Add(ErrorCode.ERR_RefLocalOrParamExpected, node.Location);
return false;
}
else if (!localSymbol.IsWritableVariable)
{
ReportReadonlyLocalError(node, localSymbol, valueKind, checkingReceiver, diagnostics);
return false;
}
}
return true;
}
}
internal partial class RefSafetyAnalysis
{
private bool CheckLocalRefEscape(SyntaxNode node, BoundLocal local, uint escapeTo, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
LocalSymbol localSymbol = local.LocalSymbol;
// if local symbol can escape to the same or wider/shallower scope then escapeTo
// then it is all ok, otherwise it is an error.
if (GetLocalScopes(localSymbol).RefEscapeScope <= escapeTo)
{
return true;
}
var inUnsafeRegion = _inUnsafeRegion;
if (escapeTo is CallingMethodScope or ReturnOnlyScope)
{
if (localSymbol.RefKind == RefKind.None)
{
if (checkingReceiver)
{
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_RefReturnLocal2 : ErrorCode.ERR_RefReturnLocal2, local.Syntax, localSymbol);
}
else
{
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_RefReturnLocal : ErrorCode.ERR_RefReturnLocal, node, localSymbol);
}
return inUnsafeRegion;
}
if (checkingReceiver)
{
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_RefReturnNonreturnableLocal2 : ErrorCode.ERR_RefReturnNonreturnableLocal2, local.Syntax, localSymbol);
}
else
{
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_RefReturnNonreturnableLocal : ErrorCode.ERR_RefReturnNonreturnableLocal, node, localSymbol);
}
return inUnsafeRegion;
}
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_EscapeVariable : ErrorCode.ERR_EscapeVariable, node, localSymbol);
return inUnsafeRegion;
}
}
internal partial class Binder
{
private bool CheckParameterValueKind(SyntaxNode node, BoundParameter parameter, BindValueKind valueKind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
Debug.Assert(!RequiresAssignableVariable(BindValueKind.AddressOf));
CheckAddressOfInAsyncOrIteratorMethod(node, valueKind, diagnostics);
ParameterSymbol parameterSymbol = parameter.ParameterSymbol;
// all parameters can be passed by ref/out or assigned to
// except "in" and "ref readonly" parameters, which are readonly
if (parameterSymbol.RefKind is RefKind.In or RefKind.RefReadOnlyParameter && RequiresAssignableVariable(valueKind))
{
ReportReadOnlyError(parameterSymbol, node, valueKind, checkingReceiver, diagnostics);
return false;
}
else if (parameterSymbol.RefKind == RefKind.None && RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
Debug.Assert(parameterSymbol.RefKind != RefKind.None || !RequiresRefAssignableVariable(valueKind));
// It is an error to capture 'in', 'ref' or 'out' parameters.
// Skipping them to simplify the logic.
if (parameterSymbol.RefKind == RefKind.None &&
parameterSymbol.ContainingSymbol is SynthesizedPrimaryConstructor primaryConstructor &&
primaryConstructor.GetCapturedParameters().TryGetValue(parameterSymbol, out FieldSymbol backingField))
{
Debug.Assert(backingField.RefKind == RefKind.None);
Debug.Assert(!RequiresRefAssignableVariable(valueKind));
if (backingField.IsReadOnly)
{
Debug.Assert(backingField.RefKind == RefKind.None);
if (RequiresAssignableVariable(valueKind) &&
!CanModifyReadonlyField(receiverIsThis: true, backingField))
{
reportReadOnlyParameterError(parameterSymbol, node, valueKind, checkingReceiver, diagnostics);
return false;
}
}
if (RequiresAssignableVariable(valueKind) && !backingField.ContainingType.IsReferenceType && (this.ContainingMemberOrLambda as MethodSymbol)?.IsEffectivelyReadOnly == true)
{
ReportThisLvalueError(node, valueKind, isValueType: true, isPrimaryConstructorParameter: true, diagnostics);
return false;
}
}
if (this.LockedOrDisposedVariables.Contains(parameterSymbol))
{
// Consider: It would be more conventional to pass "symbol" rather than "symbol.Name".
// The issue is that the error SymbolDisplayFormat doesn't display parameter
// names - only their types - which works great in signatures, but not at all
// at the top level.
diagnostics.Add(ErrorCode.WRN_AssignmentToLockOrDispose, parameter.Syntax.Location, parameterSymbol.Name);
}
return true;
}
static void reportReadOnlyParameterError(ParameterSymbol parameterSymbol, SyntaxNode node, BindValueKind valueKind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
// It's clearer to say that the address can't be taken than to say that the field can't be modified
// (even though the latter message gives more explanation of why).
Debug.Assert(valueKind != BindValueKind.AddressOf); // If this assert fails, we probably should report ErrorCode.ERR_InvalidAddrOp
if (checkingReceiver)
{
ErrorCode errorCode;
if (valueKind == BindValueKind.RefReturn)
{
errorCode = ErrorCode.ERR_RefReturnReadonlyPrimaryConstructorParameter2;
}
else if (RequiresRefOrOut(valueKind))
{
errorCode = ErrorCode.ERR_RefReadonlyPrimaryConstructorParameter2;
}
else
{
errorCode = ErrorCode.ERR_AssgReadonlyPrimaryConstructorParameter2;
}
Error(diagnostics, errorCode, node, parameterSymbol);
}
else
{
ErrorCode errorCode;
if (valueKind == BindValueKind.RefReturn)
{
errorCode = ErrorCode.ERR_RefReturnReadonlyPrimaryConstructorParameter;
}
else if (RequiresRefOrOut(valueKind))
{
errorCode = ErrorCode.ERR_RefReadonlyPrimaryConstructorParameter;
}
else
{
errorCode = ErrorCode.ERR_AssgReadonlyPrimaryConstructorParameter;
}
Error(diagnostics, errorCode, node);
}
}
}
internal partial class RefSafetyAnalysis
{
private static EscapeLevel? EscapeLevelFromScope(uint scope) => scope switch
{
ReturnOnlyScope => EscapeLevel.ReturnOnly,
CallingMethodScope => EscapeLevel.CallingMethod,
_ => null,
};
private static uint GetParameterValEscape(ParameterSymbol parameter)
{
return parameter switch
{
{ EffectiveScope: ScopedKind.ScopedValue } => CurrentMethodScope,
{ RefKind: RefKind.Out, UseUpdatedEscapeRules: true } => ReturnOnlyScope,
_ => CallingMethodScope
};
}
private static EscapeLevel? GetParameterValEscapeLevel(ParameterSymbol parameter) =>
EscapeLevelFromScope(GetParameterValEscape(parameter));
private static uint GetParameterRefEscape(ParameterSymbol parameter)
{
return parameter switch
{
{ RefKind: RefKind.None } => CurrentMethodScope,
{ EffectiveScope: ScopedKind.ScopedRef } => CurrentMethodScope,
{ HasUnscopedRefAttribute: true, RefKind: RefKind.Out } => ReturnOnlyScope,
{ HasUnscopedRefAttribute: true, IsThis: false } => CallingMethodScope,
_ => ReturnOnlyScope
};
}
private static EscapeLevel? GetParameterRefEscapeLevel(ParameterSymbol parameter) =>
EscapeLevelFromScope(GetParameterRefEscape(parameter));
private bool CheckParameterValEscape(SyntaxNode node, ParameterSymbol parameter, uint escapeTo, BindingDiagnosticBag diagnostics)
{
if (_useUpdatedEscapeRules)
{
if (GetParameterValEscape(parameter) > escapeTo)
{
Error(diagnostics, _inUnsafeRegion ? ErrorCode.WRN_EscapeVariable : ErrorCode.ERR_EscapeVariable, node, parameter);
return _inUnsafeRegion;
}
return true;
}
else
{
// always returnable
return true;
}
}
private bool CheckParameterRefEscape(SyntaxNode node, BoundExpression parameter, ParameterSymbol parameterSymbol, uint escapeTo, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
var refSafeToEscape = GetParameterRefEscape(parameterSymbol);
if (refSafeToEscape > escapeTo)
{
var isRefScoped = parameterSymbol.EffectiveScope == ScopedKind.ScopedRef;
Debug.Assert(parameterSymbol.RefKind == RefKind.None || isRefScoped || refSafeToEscape == ReturnOnlyScope);
var inUnsafeRegion = _inUnsafeRegion;
if (parameter is BoundThisReference)
{
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_RefReturnStructThis : ErrorCode.ERR_RefReturnStructThis, node);
return inUnsafeRegion;
}
#pragma warning disable format
var (errorCode, syntax) = (checkingReceiver, isRefScoped, inUnsafeRegion, refSafeToEscape) switch
{
(checkingReceiver: true, isRefScoped: true, inUnsafeRegion: false, _) => (ErrorCode.ERR_RefReturnScopedParameter2, parameter.Syntax),
(checkingReceiver: true, isRefScoped: true, inUnsafeRegion: true, _) => (ErrorCode.WRN_RefReturnScopedParameter2, parameter.Syntax),
(checkingReceiver: true, isRefScoped: false, inUnsafeRegion: false, ReturnOnlyScope) => (ErrorCode.ERR_RefReturnOnlyParameter2, parameter.Syntax),
(checkingReceiver: true, isRefScoped: false, inUnsafeRegion: true, ReturnOnlyScope) => (ErrorCode.WRN_RefReturnOnlyParameter2, parameter.Syntax),
(checkingReceiver: true, isRefScoped: false, inUnsafeRegion: false, _) => (ErrorCode.ERR_RefReturnParameter2, parameter.Syntax),
(checkingReceiver: true, isRefScoped: false, inUnsafeRegion: true, _) => (ErrorCode.WRN_RefReturnParameter2, parameter.Syntax),
(checkingReceiver: false, isRefScoped: true, inUnsafeRegion: false, _) => (ErrorCode.ERR_RefReturnScopedParameter, node),
(checkingReceiver: false, isRefScoped: true, inUnsafeRegion: true, _) => (ErrorCode.WRN_RefReturnScopedParameter, node),
(checkingReceiver: false, isRefScoped: false, inUnsafeRegion: false, ReturnOnlyScope) => (ErrorCode.ERR_RefReturnOnlyParameter, node),
(checkingReceiver: false, isRefScoped: false, inUnsafeRegion: true, ReturnOnlyScope) => (ErrorCode.WRN_RefReturnOnlyParameter, node),
(checkingReceiver: false, isRefScoped: false, inUnsafeRegion: false, _) => (ErrorCode.ERR_RefReturnParameter, node),
(checkingReceiver: false, isRefScoped: false, inUnsafeRegion: true, _) => (ErrorCode.WRN_RefReturnParameter, node)
};
#pragma warning restore format
Error(diagnostics, errorCode, syntax, parameterSymbol.Name);
return inUnsafeRegion;
}
// can ref-escape to any scope otherwise
return true;
}
}
internal partial class Binder
{
private bool CheckFieldValueKind(SyntaxNode node, BoundFieldAccess fieldAccess, BindValueKind valueKind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
var fieldSymbol = fieldAccess.FieldSymbol;
if (fieldSymbol.IsReadOnly)
{
// A field is writeable unless
// (1) it is readonly and we are not in a constructor or field initializer
// (2) the receiver of the field is of value type and is not a variable or object creation expression.
// For example, if you have a class C with readonly field f of type S, and
// S has a mutable field x, then c.f.x is not a variable because c.f is not
// writable.
if ((fieldSymbol.RefKind == RefKind.None ? RequiresAssignableVariable(valueKind) : RequiresRefAssignableVariable(valueKind)) &&
!CanModifyReadonlyField(fieldAccess.ReceiverOpt is BoundThisReference, fieldSymbol))
{
ReportReadOnlyFieldError(fieldSymbol, node, valueKind, checkingReceiver, diagnostics);
return false;
}
}
if (RequiresAssignableVariable(valueKind))
{
switch (fieldSymbol.RefKind)
{
case RefKind.None:
break;
case RefKind.Ref:
return true;
case RefKind.RefReadOnly:
ReportReadOnlyError(fieldSymbol, node, valueKind, checkingReceiver, diagnostics);
return false;
default:
throw ExceptionUtilities.UnexpectedValue(fieldSymbol.RefKind);
}
if (fieldSymbol.IsFixedSizeBuffer)
{
Error(diagnostics, GetStandardLvalueError(valueKind), node);
return false;
}
}
if (RequiresRefAssignableVariable(valueKind))
{
Debug.Assert(!fieldSymbol.IsStatic);
Debug.Assert(valueKind == BindValueKind.RefAssignable);
switch (fieldSymbol.RefKind)
{
case RefKind.None:
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
case RefKind.Ref:
case RefKind.RefReadOnly:
return CheckIsValidReceiverForVariable(node, fieldAccess.ReceiverOpt, BindValueKind.Assignable, diagnostics);
default:
throw ExceptionUtilities.UnexpectedValue(fieldSymbol.RefKind);
}
}
if (RequiresReferenceToLocation(valueKind))
{
switch (fieldSymbol.RefKind)
{
case RefKind.None:
break;
case RefKind.Ref:
case RefKind.RefReadOnly:
// ref readonly access to a ref (readonly) field is fine regardless of the receiver
return true;
default:
throw ExceptionUtilities.UnexpectedValue(fieldSymbol.RefKind);
}
}
// r/w fields that are static or belong to reference types are writeable and returnable
if (fieldSymbol.IsStatic || fieldSymbol.ContainingType.IsReferenceType)
{
return true;
}
// for other fields defer to the receiver.
return CheckIsValidReceiverForVariable(node, fieldAccess.ReceiverOpt, valueKind, diagnostics);
}
private bool CanModifyReadonlyField(bool receiverIsThis, FieldSymbol fieldSymbol)
{
// A field is writeable unless
// (1) it is readonly and we are not in a constructor or field initializer
// (2) the receiver of the field is of value type and is not a variable or object creation expression.
// For example, if you have a class C with readonly field f of type S, and
// S has a mutable field x, then c.f.x is not a variable because c.f is not
// writable.
var fieldIsStatic = fieldSymbol.IsStatic;
var canModifyReadonly = false;
Symbol containing = this.ContainingMemberOrLambda;
if ((object)containing != null &&
fieldIsStatic == containing.IsStatic &&
(fieldIsStatic || receiverIsThis) &&
(Compilation.FeatureStrictEnabled
? TypeSymbol.Equals(fieldSymbol.ContainingType, containing.ContainingType, TypeCompareKind.AllIgnoreOptions)
// We duplicate a bug in the native compiler for compatibility in non-strict mode
: TypeSymbol.Equals(fieldSymbol.ContainingType.OriginalDefinition, containing.ContainingType.OriginalDefinition, TypeCompareKind.AllIgnoreOptions)))
{
if (containing.Kind == SymbolKind.Method)
{
MethodSymbol containingMethod = (MethodSymbol)containing;
MethodKind desiredMethodKind = fieldIsStatic ? MethodKind.StaticConstructor : MethodKind.Constructor;
canModifyReadonly = (containingMethod.MethodKind == desiredMethodKind) ||
isAssignedFromInitOnlySetterOnThis(receiverIsThis);
}
else if (containing.Kind == SymbolKind.Field)
{
canModifyReadonly = true;
}
}
return canModifyReadonly;
bool isAssignedFromInitOnlySetterOnThis(bool receiverIsThis)
{
// bad: other.readonlyField = ...
// bad: base.readonlyField = ...
if (!receiverIsThis)
{
return false;
}
if (!(ContainingMemberOrLambda is MethodSymbol method))
{
return false;
}
return method.IsInitOnly;
}
}
private bool CheckSimpleAssignmentValueKind(SyntaxNode node, BoundAssignmentOperator assignment, BindValueKind valueKind, BindingDiagnosticBag diagnostics)
{
// Only ref-assigns produce LValues
if (assignment.IsRef)
{
return CheckValueKind(node, assignment.Left, valueKind, checkingReceiver: false, diagnostics);
}
Error(diagnostics, GetStandardLvalueError(valueKind), node);
return false;
}
}
internal partial class RefSafetyAnalysis
{
private uint GetFieldRefEscape(BoundFieldAccess fieldAccess, uint scopeOfTheContainingExpression)
{
var fieldSymbol = fieldAccess.FieldSymbol;
// fields that are static or belong to reference types can ref escape anywhere
if (fieldSymbol.IsStatic || fieldSymbol.ContainingType.IsReferenceType)
{
return CallingMethodScope;
}
if (_useUpdatedEscapeRules)
{
// SPEC: If `F` is a `ref` field its ref-safe-to-escape scope is the safe-to-escape scope of `e`.
if (fieldSymbol.RefKind != RefKind.None)
{
return GetValEscape(fieldAccess.ReceiverOpt, scopeOfTheContainingExpression);
}
}
// for other fields defer to the receiver.
return GetRefEscape(fieldAccess.ReceiverOpt, scopeOfTheContainingExpression);
}
private bool CheckFieldRefEscape(SyntaxNode node, BoundFieldAccess fieldAccess, uint escapeFrom, uint escapeTo, BindingDiagnosticBag diagnostics)
{
var fieldSymbol = fieldAccess.FieldSymbol;
// fields that are static or belong to reference types can ref escape anywhere
if (fieldSymbol.IsStatic || fieldSymbol.ContainingType.IsReferenceType)
{
return true;
}
Debug.Assert(fieldAccess.ReceiverOpt is { });
if (_useUpdatedEscapeRules)
{
// SPEC: If `F` is a `ref` field its ref-safe-to-escape scope is the safe-to-escape scope of `e`.
if (fieldSymbol.RefKind != RefKind.None)
{
return CheckValEscape(node, fieldAccess.ReceiverOpt, escapeFrom, escapeTo, checkingReceiver: true, diagnostics);
}
}
// for other fields defer to the receiver.
return CheckRefEscape(node, fieldAccess.ReceiverOpt, escapeFrom, escapeTo, checkingReceiver: true, diagnostics: diagnostics);
}
private bool CheckFieldLikeEventRefEscape(SyntaxNode node, BoundEventAccess eventAccess, uint escapeFrom, uint escapeTo, BindingDiagnosticBag diagnostics)
{
var eventSymbol = eventAccess.EventSymbol;
// field-like events that are static or belong to reference types can ref escape anywhere
if (eventSymbol.IsStatic || eventSymbol.ContainingType.IsReferenceType)
{
return true;
}
// for other events defer to the receiver.
return CheckRefEscape(node, eventAccess.ReceiverOpt, escapeFrom, escapeTo, checkingReceiver: true, diagnostics: diagnostics);
}
}
internal partial class Binder
{
private bool CheckEventValueKind(BoundEventAccess boundEvent, BindValueKind valueKind, BindingDiagnosticBag diagnostics)
{
// Compound assignment (actually "event assignment") is allowed "everywhere", subject to the restrictions of
// accessibility, use site errors, and receiver variable-ness (for structs).
// Other operations are allowed only for field-like events and only where the backing field is accessible
// (i.e. in the declaring type) - subject to use site errors and receiver variable-ness.
BoundExpression receiver = boundEvent.ReceiverOpt;
SyntaxNode eventSyntax = GetEventName(boundEvent); //does not include receiver
EventSymbol eventSymbol = boundEvent.EventSymbol;
if (valueKind == BindValueKind.CompoundAssignment)
{
// NOTE: accessibility has already been checked by lookup.
// NOTE: availability of well-known members is checked in BindEventAssignment because
// we don't have the context to determine whether addition or subtraction is being performed.
if (ReportUseSite(eventSymbol, diagnostics, eventSyntax))
{
// NOTE: BindEventAssignment checks use site errors on the specific accessor
// (since we don't know which is being used).
return false;
}
Debug.Assert(!RequiresVariableReceiver(receiver, eventSymbol));
return true;
}
else
{
if (!boundEvent.IsUsableAsField)
{
// Dev10 reports this in addition to ERR_BadAccess, but we won't even reach this point if the event isn't accessible (caught by lookup).
Error(diagnostics, GetBadEventUsageDiagnosticInfo(eventSymbol), eventSyntax);
return false;
}
else if (ReportUseSite(eventSymbol, diagnostics, eventSyntax))
{
if (!CheckIsValidReceiverForVariable(eventSyntax, receiver, BindValueKind.Assignable, diagnostics))
{
return false;
}
}
else if (RequiresVariable(valueKind))
{
if (eventSymbol.IsWindowsRuntimeEvent && valueKind != BindValueKind.Assignable)
{
// NOTE: Dev11 reports ERR_RefProperty, as if this were a property access (since that's how it will be lowered).
// Roslyn reports a new, more specific, error code.
if (valueKind == BindValueKind.RefOrOut)
{
Error(diagnostics, ErrorCode.ERR_WinRtEventPassedByRef, eventSyntax);
}
else
{
Error(diagnostics, GetStandardLvalueError(valueKind), eventSyntax, eventSymbol);
}
return false;
}
else if (RequiresVariableReceiver(receiver, eventSymbol.AssociatedField) && // NOTE: using field, not event
!CheckIsValidReceiverForVariable(eventSyntax, receiver, valueKind, diagnostics))
{
return false;
}
}
return true;
}
}
private bool CheckIsValidReceiverForVariable(SyntaxNode node, BoundExpression receiver, BindValueKind kind, BindingDiagnosticBag diagnostics)
{
Debug.Assert(receiver != null);
return Flags.Includes(BinderFlags.ObjectInitializerMember) && receiver.Kind == BoundKind.ObjectOrCollectionValuePlaceholder ||
CheckValueKind(node, receiver, kind, true, diagnostics);
}
/// <summary>
/// SPEC: When a property or indexer declared in a struct-type is the target of an
/// SPEC: assignment, the instance expression associated with the property or indexer
/// SPEC: access must be classified as a variable. If the instance expression is
/// SPEC: classified as a value, a compile-time error occurs. Because of 7.6.4,
/// SPEC: the same rule also applies to fields.
/// </summary>
/// <remarks>
/// NOTE: The spec fails to impose the restriction that the event receiver must be classified
/// as a variable (unlike for properties - 7.17.1). This seems like a bug, but we have
/// production code that won't build with the restriction in place (see DevDiv #15674).
/// </remarks>
private static bool RequiresVariableReceiver(BoundExpression receiver, Symbol symbol)
{
return symbol.RequiresInstanceReceiver()
&& symbol.Kind != SymbolKind.Event
&& receiver?.Type?.IsValueType == true;
}
protected bool CheckMethodReturnValueKind(
MethodSymbol methodSymbol,
SyntaxNode callSyntaxOpt,
SyntaxNode node,
BindValueKind valueKind,
bool checkingReceiver,
BindingDiagnosticBag diagnostics)
{
// A call can only be a variable if it returns by reference. If this is the case,
// whether or not it is a valid variable depends on whether or not the call is the
// RHS of a return or an assign by reference:
// - If call is used in a context demanding ref-returnable reference all of its ref
// inputs must be ref-returnable
if (RequiresVariable(valueKind) && methodSymbol.RefKind == RefKind.None)
{
if (checkingReceiver)
{
// Error is associated with expression, not node which may be distinct.
Error(diagnostics, ErrorCode.ERR_ReturnNotLValue, callSyntaxOpt, methodSymbol);
}
else
{
Error(diagnostics, GetStandardLvalueError(valueKind), node);
}
return false;
}
if (RequiresAssignableVariable(valueKind) && methodSymbol.RefKind == RefKind.RefReadOnly)
{
ReportReadOnlyError(methodSymbol, node, valueKind, checkingReceiver, diagnostics);
return false;
}
if (RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
return true;
}
private bool CheckPropertyValueKind(SyntaxNode node, BoundExpression expr, BindValueKind valueKind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
// SPEC: If the left operand is a property or indexer access, the property or indexer must
// SPEC: have a set accessor. If this is not the case, a compile-time error occurs.
// Addendum: Assignment is also allowed for get-only autoprops in their constructor
BoundExpression receiver;
SyntaxNode propertySyntax;
var propertySymbol = GetPropertySymbol(expr, out receiver, out propertySyntax);
Debug.Assert((object)propertySymbol != null);
Debug.Assert(propertySyntax != null);
if ((RequiresReferenceToLocation(valueKind) || checkingReceiver) &&
propertySymbol.RefKind == RefKind.None)
{
if (checkingReceiver)
{
// Error is associated with expression, not node which may be distinct.
// This error is reported for all values types. That is a breaking
// change from Dev10 which reports this error for struct types only,
// not for type parameters constrained to "struct".
Debug.Assert(propertySymbol.TypeWithAnnotations.HasType);
Error(diagnostics, ErrorCode.ERR_ReturnNotLValue, expr.Syntax, propertySymbol);
}
else if (valueKind == BindValueKind.RefOrOut)
{
Error(diagnostics, ErrorCode.ERR_RefProperty, node);
}
else
{
Error(diagnostics, GetStandardLvalueError(valueKind), node);
}
return false;
}
if (RequiresAssignableVariable(valueKind) && propertySymbol.RefKind == RefKind.RefReadOnly)
{
ReportReadOnlyError(propertySymbol, node, valueKind, checkingReceiver, diagnostics);
return false;
}
var requiresSet = RequiresAssignableVariable(valueKind) && propertySymbol.RefKind == RefKind.None;
if (requiresSet)
{
var setMethod = propertySymbol.GetOwnOrInheritedSetMethod();
if (setMethod is null)
{
var containing = this.ContainingMemberOrLambda;
if (!AccessingAutoPropertyFromConstructor(receiver, propertySymbol, containing, AccessorKind.Set)
&& !isAllowedDespiteReadonly(receiver))
{
Error(diagnostics, ErrorCode.ERR_AssgReadonlyProp, node, propertySymbol);
return false;
}
}
else
{
if (setMethod.IsInitOnly)
{
if (!isAllowedInitOnlySet(receiver))
{
Error(diagnostics, ErrorCode.ERR_AssignmentInitOnly, node, propertySymbol);
return false;
}
if (setMethod.DeclaringCompilation != this.Compilation)
{
// an error would have already been reported on declaring an init-only setter
CheckFeatureAvailability(node, MessageID.IDS_FeatureInitOnlySetters, diagnostics);
}
}
var accessThroughType = this.GetAccessThroughType(receiver);
bool failedThroughTypeCheck;
CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
bool isAccessible = this.IsAccessible(setMethod, accessThroughType, out failedThroughTypeCheck, ref useSiteInfo);
diagnostics.Add(node, useSiteInfo);
if (!isAccessible)
{
if (failedThroughTypeCheck)
{
Error(diagnostics, ErrorCode.ERR_BadProtectedAccess, node, propertySymbol, accessThroughType, this.ContainingType);
}
else
{
Error(diagnostics, ErrorCode.ERR_InaccessibleSetter, node, propertySymbol);
}
return false;
}
ReportDiagnosticsIfObsolete(diagnostics, setMethod, node, receiver?.Kind == BoundKind.BaseReference);
var setValueKind = setMethod.IsEffectivelyReadOnly ? BindValueKind.RValue : BindValueKind.Assignable;
if (RequiresVariableReceiver(receiver, setMethod) && !CheckIsValidReceiverForVariable(node, receiver, setValueKind, diagnostics))
{
return false;
}
if (IsBadBaseAccess(node, receiver, setMethod, diagnostics, propertySymbol) ||
reportUseSite(setMethod))
{
return false;
}
CheckReceiverAndRuntimeSupportForSymbolAccess(node, receiver, setMethod, diagnostics);
}
}
var requiresGet = !RequiresAssignmentOnly(valueKind) || propertySymbol.RefKind != RefKind.None;
if (requiresGet)
{
var getMethod = propertySymbol.GetOwnOrInheritedGetMethod();
if ((object)getMethod == null)
{
Error(diagnostics, ErrorCode.ERR_PropertyLacksGet, node, propertySymbol);
return false;
}
else
{
var accessThroughType = this.GetAccessThroughType(receiver);
bool failedThroughTypeCheck;
CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
bool isAccessible = this.IsAccessible(getMethod, accessThroughType, out failedThroughTypeCheck, ref useSiteInfo);
diagnostics.Add(node, useSiteInfo);
if (!isAccessible)
{
if (failedThroughTypeCheck)
{
Error(diagnostics, ErrorCode.ERR_BadProtectedAccess, node, propertySymbol, accessThroughType, this.ContainingType);
}
else
{
Error(diagnostics, ErrorCode.ERR_InaccessibleGetter, node, propertySymbol);
}
return false;
}
CheckImplicitThisCopyInReadOnlyMember(receiver, getMethod, diagnostics);
ReportDiagnosticsIfObsolete(diagnostics, getMethod, node, receiver?.Kind == BoundKind.BaseReference);
if (IsBadBaseAccess(node, receiver, getMethod, diagnostics, propertySymbol) ||
reportUseSite(getMethod))
{
return false;
}
CheckReceiverAndRuntimeSupportForSymbolAccess(node, receiver, getMethod, diagnostics);
}
}
if (RequiresRefAssignableVariable(valueKind))
{
Error(diagnostics, ErrorCode.ERR_RefLocalOrParamExpected, node);
return false;
}
return true;
bool reportUseSite(MethodSymbol accessor)
{
UseSiteInfo<AssemblySymbol> useSiteInfo = accessor.GetUseSiteInfo();
if (!object.Equals(useSiteInfo.DiagnosticInfo, propertySymbol.GetUseSiteInfo().DiagnosticInfo))
{
return diagnostics.Add(useSiteInfo, propertySyntax);
}
else
{
diagnostics.AddDependencies(useSiteInfo);
}
return false;
}
static bool isAllowedDespiteReadonly(BoundExpression receiver)
{
// ok: anonymousType with { Property = ... }
if (receiver is BoundObjectOrCollectionValuePlaceholder && receiver.Type.IsAnonymousType)
{
return true;
}
return false;
}
bool isAllowedInitOnlySet(BoundExpression receiver)
{
// ok: new C() { InitOnlyProperty = ... }
// bad: { ... = { InitOnlyProperty = ... } }
if (receiver is BoundObjectOrCollectionValuePlaceholder placeholder)
{
return placeholder.IsNewInstance;
}
// bad: other.InitOnlyProperty = ...
if (!(receiver is BoundThisReference || receiver is BoundBaseReference))
{
return false;
}
var containingMember = ContainingMemberOrLambda;
if (!(containingMember is MethodSymbol method))
{
return false;
}
if (method.MethodKind == MethodKind.Constructor || method.IsInitOnly)
{
// ok: setting on `this` or `base` from an instance constructor or init-only setter
return true;
}
return false;
}
}
private bool IsBadBaseAccess(SyntaxNode node, BoundExpression receiverOpt, Symbol member, BindingDiagnosticBag diagnostics,
Symbol propertyOrEventSymbolOpt = null)
{
Debug.Assert(member.Kind != SymbolKind.Property);
Debug.Assert(member.Kind != SymbolKind.Event);
if (receiverOpt?.Kind == BoundKind.BaseReference && member.IsAbstract)
{
Error(diagnostics, ErrorCode.ERR_AbstractBaseCall, node, propertyOrEventSymbolOpt ?? member);
return true;
}
return false;
}
}
internal partial class RefSafetyAnalysis
{
internal uint GetInterpolatedStringHandlerConversionEscapeScope(
BoundExpression expression,
uint scopeOfTheContainingExpression)
{
var data = expression.GetInterpolatedStringHandlerData();
#if DEBUG
// VisitArgumentsAndGetArgumentPlaceholders() does not visit data.Construction
// since that expression does not introduce locals or placeholders that are needed
// by GetValEscape() or CheckValEscape(), so we disable tracking here.
var previousVisited = _visited;
_visited = null;
#endif
uint escapeScope = GetValEscape(data.Construction, scopeOfTheContainingExpression);
#if DEBUG
_visited = previousVisited;
#endif
var arguments = ArrayBuilder<BoundExpression>.GetInstance();
GetInterpolatedStringHandlerArgumentsForEscape(expression, arguments);
foreach (var argument in arguments)
{
uint argEscape = GetValEscape(argument, scopeOfTheContainingExpression);
escapeScope = Math.Max(escapeScope, argEscape);
}
arguments.Free();
return escapeScope;
}
#nullable enable
/// <summary>
/// Computes the scope to which the given invocation can escape
/// NOTE: the escape scope for ref and val escapes is the same for invocations except for trivial cases (ordinary type returned by val)
/// where escape is known otherwise. Therefore we do not have two ref/val variants of this.
///
/// NOTE: we need scopeOfTheContainingExpression as some expressions such as optional <c>in</c> parameters or <c>ref dynamic</c> behave as
/// local variables declared at the scope of the invocation.
/// </summary>
private uint GetInvocationEscapeScope(
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
uint scopeOfTheContainingExpression,
bool isRefEscape
)
{
#if DEBUG
Debug.Assert(AllParametersConsideredInEscapeAnalysisHaveArguments(argsOpt, parameters, argsToParamsOpt));
#endif
if (methodInfo.UseUpdatedEscapeRules)
{
return GetInvocationEscapeWithUpdatedRules(methodInfo, receiver, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, scopeOfTheContainingExpression, isRefEscape);
}
// SPEC: (also applies to the CheckInvocationEscape counterpart)
//
// An lvalue resulting from a ref-returning method invocation e1.M(e2, ...) is ref-safe - to - escape the smallest of the following scopes:
//• The entire enclosing method
//• the ref-safe-to-escape of all ref/out/in argument expressions(excluding the receiver)
//• the safe-to - escape of all argument expressions(including the receiver)
//
// An rvalue resulting from a method invocation e1.M(e2, ...) is safe - to - escape from the smallest of the following scopes:
//• The entire enclosing method
//• the safe-to-escape of all argument expressions(including the receiver)
//
uint escapeScope = CallingMethodScope;
var escapeValues = ArrayBuilder<EscapeValue>.GetInstance();
GetEscapeValuesForOldRules(
in methodInfo,
// Receiver handled explicitly below
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
// ref kinds of varargs are not interesting here.
// __refvalue is not ref-returnable, so ref varargs can't come back from a call
ignoreArglistRefKinds: true,
mixableArguments: null,
escapeValues);
try
{
foreach (var (parameter, argument, _, argumentIsRefEscape) in escapeValues)
{
// ref escape scope is the narrowest of
// - ref escape of all byref arguments
// - val escape of all byval arguments (ref-like values can be unwrapped into refs, so treat val escape of values as possible ref escape of the result)
//
// val escape scope is the narrowest of
// - val escape of all byval arguments (refs cannot be wrapped into values, so their ref escape is irrelevant, only use val escapes)
uint argumentEscape = (isRefEscape, argumentIsRefEscape) switch
{
(true, true) => GetRefEscape(argument, scopeOfTheContainingExpression),
(false, false) => GetValEscape(argument, scopeOfTheContainingExpression),
_ => escapeScope
};
escapeScope = Math.Max(escapeScope, argumentEscape);
if (escapeScope >= scopeOfTheContainingExpression)
{
// can't get any worse
return escapeScope;
}
}
}
finally
{
escapeValues.Free();
}
// check receiver if ref-like
if (methodInfo.Method?.RequiresInstanceReceiver == true && receiver?.Type?.IsRefLikeOrAllowsRefLikeType() == true)
{
escapeScope = Math.Max(escapeScope, GetValEscape(receiver, scopeOfTheContainingExpression));
}
return escapeScope;
}
private uint GetInvocationEscapeWithUpdatedRules(
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
uint scopeOfTheContainingExpression,
bool isRefEscape)
{
//by default it is safe to escape
uint escapeScope = CallingMethodScope;
var argsAndParamsAll = ArrayBuilder<EscapeValue>.GetInstance();
GetFilteredInvocationArgumentsForEscapeWithUpdatedRules(
methodInfo,
receiver,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
isRefEscape,
ignoreArglistRefKinds: true, // https://github.com/dotnet/roslyn/issues/63325: for compatibility with C#10 implementation.
argsAndParamsAll);
var returnsRefToRefStruct = methodInfo.ReturnsRefToRefStruct;
foreach (var (param, argument, _, isArgumentRefEscape) in argsAndParamsAll)
{
// SPEC:
// If `M()` does return ref-to-ref-struct, the *safe-to-escape* is the same as the *safe-to-escape* of all arguments which are ref-to-ref-struct. It is an error if there are multiple arguments with different *safe-to-escape* because of *method arguments must match*.
// If `M()` does return ref-to-ref-struct, the *ref-safe-to-escape* is the narrowest *ref-safe-to-escape* contributed by all arguments which are ref-to-ref-struct.
//
if (!returnsRefToRefStruct
|| ((param is null ||
(param is { RefKind: not RefKind.None, Type: { } type } && type.IsRefLikeOrAllowsRefLikeType())) &&
isArgumentRefEscape == isRefEscape))
{
uint argEscape = isArgumentRefEscape ?
GetRefEscape(argument, scopeOfTheContainingExpression) :
GetValEscape(argument, scopeOfTheContainingExpression);
escapeScope = Math.Max(escapeScope, argEscape);
if (escapeScope >= scopeOfTheContainingExpression)
{
// can't get any worse
break;
}
}
}
argsAndParamsAll.Free();
return escapeScope;
}
/// <summary>
/// Validates whether given invocation can allow its results to escape from <paramref name="escapeFrom"/> level to <paramref name="escapeTo"/> level.
/// The result indicates whether the escape is possible.
/// Additionally, the method emits diagnostics (possibly more than one, recursively) that would help identify the cause for the failure.
///
/// NOTE: we need scopeOfTheContainingExpression as some expressions such as optional <c>in</c> parameters or <c>ref dynamic</c> behave as
/// local variables declared at the scope of the invocation.
/// </summary>
private bool CheckInvocationEscape(
SyntaxNode syntax,
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
bool checkingReceiver,
uint escapeFrom,
uint escapeTo,
BindingDiagnosticBag diagnostics,
bool isRefEscape
)
{
#if DEBUG
Debug.Assert(AllParametersConsideredInEscapeAnalysisHaveArguments(argsOpt, parameters, argsToParamsOpt));
#endif
if (methodInfo.UseUpdatedEscapeRules)
{
return CheckInvocationEscapeWithUpdatedRules(syntax, methodInfo, receiver, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, checkingReceiver, escapeFrom, escapeTo, diagnostics, isRefEscape);
}
// SPEC:
// In a method invocation, the following constraints apply:
//• If there is a ref or out argument to a ref struct type (including the receiver), with safe-to-escape E1, then
// o no ref or out argument(excluding the receiver and arguments of ref-like types) may have a narrower ref-safe-to-escape than E1; and
// o no argument(including the receiver) may have a narrower safe-to-escape than E1.
var symbol = methodInfo.Symbol;
if (!symbol.RequiresInstanceReceiver())
{
// ignore receiver when symbol is static
receiver = null;
}
var escapeArguments = ArrayBuilder<EscapeArgument>.GetInstance();
GetInvocationArgumentsForEscape(
methodInfo,
receiver: null, // receiver handled explicitly below
receiverIsSubjectToCloning: ThreeState.Unknown,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
// ref kinds of varargs are not interesting here.
// __refvalue is not ref-returnable, so ref varargs can't come back from a call
ignoreArglistRefKinds: true,
mixableArguments: null,
escapeArguments);
try
{
foreach (var (parameter, argument, effectiveRefKind) in escapeArguments)
{
// ref escape scope is the narrowest of
// - ref escape of all byref arguments
// - val escape of all byval arguments (ref-like values can be unwrapped into refs, so treat val escape of values as possible ref escape of the result)
//
// val escape scope is the narrowest of
// - val escape of all byval arguments (refs cannot be wrapped into values, so their ref escape is irrelevant, only use val escapes)
var valid = effectiveRefKind != RefKind.None && isRefEscape ?
CheckRefEscape(argument.Syntax, argument, escapeFrom, escapeTo, false, diagnostics) :
CheckValEscape(argument.Syntax, argument, escapeFrom, escapeTo, false, diagnostics);
if (!valid)
{
if (symbol is not SignatureOnlyMethodSymbol)
{
ReportInvocationEscapeError(syntax, symbol, parameter, checkingReceiver, diagnostics);
}
return false;
}
}
}
finally
{
escapeArguments.Free();
}
// check receiver if ref-like
if (receiver?.Type?.IsRefLikeOrAllowsRefLikeType() == true)
{
return CheckValEscape(receiver.Syntax, receiver, escapeFrom, escapeTo, false, diagnostics);
}
return true;
}
private bool CheckInvocationEscapeWithUpdatedRules(
SyntaxNode syntax,
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
bool checkingReceiver,
uint escapeFrom,
uint escapeTo,
BindingDiagnosticBag diagnostics,
bool isRefEscape)
{
bool result = true;
var argsAndParamsAll = ArrayBuilder<EscapeValue>.GetInstance();
GetFilteredInvocationArgumentsForEscapeWithUpdatedRules(
methodInfo,
receiver,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
isRefEscape,
ignoreArglistRefKinds: true, // https://github.com/dotnet/roslyn/issues/63325: for compatibility with C#10 implementation.
argsAndParamsAll);
var symbol = methodInfo.Symbol;
var returnsRefToRefStruct = methodInfo.ReturnsRefToRefStruct;
foreach (var (param, argument, _, isArgumentRefEscape) in argsAndParamsAll)
{
// SPEC:
// If `M()` does return ref-to-ref-struct, the *safe-to-escape* is the same as the *safe-to-escape* of all arguments which are ref-to-ref-struct. It is an error if there are multiple arguments with different *safe-to-escape* because of *method arguments must match*.
// If `M()` does return ref-to-ref-struct, the *ref-safe-to-escape* is the narrowest *ref-safe-to-escape* contributed by all arguments which are ref-to-ref-struct.
//
if (!returnsRefToRefStruct
|| ((param is null ||
(param is { RefKind: not RefKind.None, Type: { } type } && type.IsRefLikeOrAllowsRefLikeType())) &&
isArgumentRefEscape == isRefEscape))
{
bool valid = isArgumentRefEscape ?
CheckRefEscape(argument.Syntax, argument, escapeFrom, escapeTo, false, diagnostics) :
CheckValEscape(argument.Syntax, argument, escapeFrom, escapeTo, false, diagnostics);
if (!valid)
{
// For consistency with C#10 implementation, we don't report an additional error
// for the receiver. (In both implementations, the call to Check*Escape() above
// will have reported a specific escape error for the receiver though.)
if ((object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) != receiver && symbol is not SignatureOnlyMethodSymbol)
{
ReportInvocationEscapeError(syntax, symbol, param, checkingReceiver, diagnostics);
}
result = false;
break;
}
}
}
argsAndParamsAll.Free();
return result;
}
/// <summary>
/// Returns the set of arguments to be considered for escape analysis of a method invocation. This
/// set potentially includes the receiver of the method call. Each argument is returned (only once)
/// with the corresponding parameter and ref kind.
///
/// No filtering like removing non-reflike types is done by this method. It is the responsibility of
/// the caller to determine which arguments impact escape analysis.
/// </summary>
private void GetInvocationArgumentsForEscape(
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
bool ignoreArglistRefKinds,
ArrayBuilder<MixableDestination>? mixableArguments,
ArrayBuilder<EscapeArgument> escapeArguments)
{
if (receiver is { })
{
Debug.Assert(receiver.Type is { });
Debug.Assert(receiverIsSubjectToCloning != ThreeState.Unknown);
var method = methodInfo.Method;
if (receiverIsSubjectToCloning == ThreeState.True)
{
Debug.Assert(receiver is not BoundValuePlaceholderBase && method is not null && receiver.Type?.IsReferenceType == false);
#if DEBUG
AssertVisited(receiver);
#endif
// Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration
receiver = new BoundCapturedReceiverPlaceholder(receiver.Syntax, receiver, _localScopeDepth, receiver.Type).MakeCompilerGenerated();
}
var tuple = getReceiver(methodInfo, receiver);
escapeArguments.Add(tuple);
if (mixableArguments is not null && isMixableParameter(tuple.Parameter))
{
mixableArguments.Add(new MixableDestination(tuple.Parameter, receiver));
}
}
if (!argsOpt.IsDefault)
{
for (int argIndex = 0; argIndex < argsOpt.Length; argIndex++)
{
var argument = argsOpt[argIndex];
if (argument.Kind == BoundKind.ArgListOperator)
{
Debug.Assert(argIndex == argsOpt.Length - 1);
// unwrap varargs and process as more arguments
var argList = (BoundArgListOperator)argument;
getArgList(
argList.Arguments,
ignoreArglistRefKinds ? default : argList.ArgumentRefKindsOpt,
mixableArguments,
escapeArguments);
break;
}
var parameter = argIndex < parameters.Length ?
parameters[argsToParamsOpt.IsDefault ? argIndex : argsToParamsOpt[argIndex]] :
null;
if (mixableArguments is not null
&& isMixableParameter(parameter)
// assume any expression variable is a valid mixing destination,
// since we will infer a legal val-escape for it (if it doesn't already have a narrower one).
&& isMixableArgument(argument))
{
mixableArguments.Add(new MixableDestination(parameter, argument));
}
var refKind = parameter?.RefKind ?? RefKind.None;
if (!argRefKindsOpt.IsDefault)
{
refKind = argRefKindsOpt[argIndex];
}
if (refKind == RefKind.None &&
parameter?.RefKind is RefKind.In or RefKind.RefReadOnlyParameter)
{
refKind = parameter.RefKind;
}
escapeArguments.Add(new EscapeArgument(parameter, argument, refKind));
}
}
static bool isMixableParameter([NotNullWhen(true)] ParameterSymbol? parameter) =>
parameter is not null &&
parameter.Type.IsRefLikeOrAllowsRefLikeType() &&
parameter.RefKind.IsWritableReference();
static bool isMixableArgument(BoundExpression argument)
{
if (argument is BoundDeconstructValuePlaceholder { VariableSymbol: not null } or BoundLocal { DeclarationKind: not BoundLocalDeclarationKind.None })
{
return false;
}
if (argument.IsDiscardExpression())
{
return false;
}
return true;
}
static EscapeArgument getReceiver(in MethodInfo methodInfo, BoundExpression receiver)
{
// When there is compound usage the receiver is used once but both the get and
// set methods are invoked. This will prefer an accessor that has a writable
// `this` as it's more dangerous from a ref safety standpoint.
if (methodInfo.Method is not null && methodInfo.SetMethod is not null)
{
var getArgument = getReceiverCore(methodInfo.Method, receiver);
if (getArgument.RefKind == RefKind.Ref)
{
return getArgument;
}
var setArgument = getReceiverCore(methodInfo.SetMethod, receiver);
if (setArgument.RefKind == RefKind.Ref)
{
return setArgument;
}
Debug.Assert(!getArgument.RefKind.IsWritableReference());
return getArgument;
}
return getReceiverCore(methodInfo.Method, receiver);
}
static EscapeArgument getReceiverCore(MethodSymbol? method, BoundExpression receiver)
{
if (method is FunctionPointerMethodSymbol)
{
return new EscapeArgument(parameter: null, receiver, RefKind.None);
}
var refKind = RefKind.None;
ParameterSymbol? thisParameter = null;
if (method is not null &&
method.TryGetThisParameter(out thisParameter) &&
thisParameter is not null)
{
if (receiver.Type is TypeParameterSymbol typeParameter)
{
// Pretend that the type of the parameter is the type parameter
thisParameter = new TypeParameterThisParameterSymbol(thisParameter, typeParameter);
}
refKind = thisParameter.RefKind;
}
return new EscapeArgument(thisParameter, receiver, refKind);
}
static void getArgList(
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ArrayBuilder<MixableDestination>? mixableArguments,
ArrayBuilder<EscapeArgument> escapeArguments)
{
for (int argIndex = 0; argIndex < argsOpt.Length; argIndex++)
{
var argument = argsOpt[argIndex];
var refKind = argRefKindsOpt.IsDefault ? RefKind.None : argRefKindsOpt[argIndex];
escapeArguments.Add(new EscapeArgument(parameter: null, argument, refKind, isArgList: true));
if (refKind == RefKind.Ref && mixableArguments is not null)
{
mixableArguments.Add(new MixableDestination(argument, EscapeLevel.CallingMethod));
}
}
}
}
/// <summary>
/// Returns the set of arguments to be considered for escape analysis of a method
/// invocation. Each argument is returned with the correponding parameter and
/// whether analysis should consider value or ref escape. Not all method arguments
/// are included, and some arguments may be included twice - once for value, once for ref.
/// </summary>
private void GetFilteredInvocationArgumentsForEscapeWithUpdatedRules(
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
bool isInvokedWithRef,
bool ignoreArglistRefKinds,
ArrayBuilder<EscapeValue> escapeValues)
{
// This code is attempting to implement the following portion of the spec. Essentially if we're not
// either invoking a method by ref or have a ref struct return then there is no need to consider the
// argument escape scopes when calculating the return escape scope.
//
// > A value resulting from a method invocation `e1.M(e2, ...)` is *safe-to-escape* from the narrowest of the following scopes:
// > 1. The *calling method*
// > 2. When the return is a `ref struct` the *safe-to-escape* contributed by all argument expressions
// > 3. When the return is a `ref struct` the *ref-safe-to-escape* contributed by all `ref` arguments
//
// The `ref` calling rules can be simplified to:
//
// > A value resulting from a method invocation `ref e1.M(e2, ...)` is *ref-safe-to-escape* the narrowest of the following scopes:
// > 1. The *calling method*
// > 2. The *safe-to-escape* contributed by all argument expressions
// > 3. The *ref-safe-to-escape* contributed by all `ref` arguments
// If we're not invoking with ref or returning a ref struct then the spec does not consider
// any arguments hence the filter is always empty.
if (!isInvokedWithRef && !hasRefLikeReturn(methodInfo.Symbol))
{
return;
}
GetEscapeValuesForUpdatedRules(
methodInfo,
receiver,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
ignoreArglistRefKinds,
mixableArguments: null,
escapeValues);
static bool hasRefLikeReturn(Symbol symbol)
{
switch (symbol)
{
case MethodSymbol method:
if (method.MethodKind == MethodKind.Constructor)
{
return method.ContainingType.IsRefLikeType;
}
return method.ReturnType.IsRefLikeOrAllowsRefLikeType();
case PropertySymbol property:
return property.Type.IsRefLikeOrAllowsRefLikeType();
default:
return false;
}
}
}
/// <summary>
/// Returns the set of <see cref="EscapeValue"/> to an invocation that impact ref analysis.
/// This will filter out everything that could never meaningfully contribute to ref analysis.
/// </summary>
private void GetEscapeValues(
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
bool ignoreArglistRefKinds,
ArrayBuilder<MixableDestination>? mixableArguments,
ArrayBuilder<EscapeValue> escapeValues)
{
if (methodInfo.UseUpdatedEscapeRules)
{
GetEscapeValuesForUpdatedRules(
methodInfo,
receiver,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
ignoreArglistRefKinds,
mixableArguments,
escapeValues);
}
else
{
GetEscapeValuesForOldRules(
methodInfo,
receiver,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
ignoreArglistRefKinds,
mixableArguments,
escapeValues);
}
}
/// <summary>
/// Returns the set of <see cref="EscapeValue"/> to an invocation that impact ref analysis.
/// This will filter out everything that could never meaningfully contribute to ref analysis. For
/// example:
/// - For ref arguments it will return an <see cref="EscapeValue"/> for both ref and
/// value escape (if appropriate based on scoped-ness of associated parameters).
/// - It will remove value escape for args which correspond to scoped parameters.
/// - It will remove value escape for non-ref struct.
/// - It will remove ref escape for args which correspond to scoped refs.
/// Optionally this will also return all of the <see cref="MixableDestination" /> that
/// result from this invocation. That is useful for MAMM analysis.
/// </summary>
private void GetEscapeValuesForUpdatedRules(
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
bool ignoreArglistRefKinds,
ArrayBuilder<MixableDestination>? mixableArguments,
ArrayBuilder<EscapeValue> escapeValues)
{
if (!methodInfo.Symbol.RequiresInstanceReceiver())
{
// ignore receiver when symbol is static
receiver = null;
}
var escapeArguments = ArrayBuilder<EscapeArgument>.GetInstance();
GetInvocationArgumentsForEscape(
methodInfo,
receiver,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
ignoreArglistRefKinds,
mixableArguments,
escapeArguments);
foreach (var (parameter, argument, refKind) in escapeArguments)
{
// This means it's part of an __arglist or function pointer receiver.
if (parameter is null)
{
if (refKind != RefKind.None)
{
escapeValues.Add(new EscapeValue(parameter: null, argument, EscapeLevel.ReturnOnly, isRefEscape: true));
}
if (argument.Type?.IsRefLikeOrAllowsRefLikeType() == true)
{
escapeValues.Add(new EscapeValue(parameter: null, argument, EscapeLevel.CallingMethod, isRefEscape: false));
}
continue;
}
if (parameter.Type.IsRefLikeOrAllowsRefLikeType() && parameter.RefKind != RefKind.Out && GetParameterValEscapeLevel(parameter) is { } valEscapeLevel)
{
escapeValues.Add(new EscapeValue(parameter, argument, valEscapeLevel, isRefEscape: false));
}
// It's important to check values then references. Flipping will change the set of errors
// produced by MAMM because of the CheckRefEscape / CheckValEscape calls.
if (parameter.RefKind != RefKind.None && GetParameterRefEscapeLevel(parameter) is { } refEscapeLevel)
{
escapeValues.Add(new EscapeValue(parameter, argument, refEscapeLevel, isRefEscape: true));
}
}
escapeArguments.Free();
}
/// <summary>
/// Returns the set of <see cref="EscapeValue"/> to an invocation that impact ref analysis.
/// This will filter out everything that could never meaningfully contribute to ref analysis. For
/// example:
/// - For ref arguments it will return an <see cref="EscapeValue"/> for both ref and
/// value escape.
/// - It will remove value escape for non-ref struct.
/// - It will remove ref escape for args which correspond to any refs as old rules couldn't
/// escape refs
/// Note: this does not consider scoped-ness as it was not present in old rules
/// </summary>
private void GetEscapeValuesForOldRules(
in MethodInfo methodInfo,
BoundExpression? receiver,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
bool ignoreArglistRefKinds,
ArrayBuilder<MixableDestination>? mixableArguments,
ArrayBuilder<EscapeValue> escapeValues)
{
if (!methodInfo.Symbol.RequiresInstanceReceiver())
{
// ignore receiver when symbol is static
receiver = null;
}
var escapeArguments = ArrayBuilder<EscapeArgument>.GetInstance();
GetInvocationArgumentsForEscape(
methodInfo,
receiver,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
ignoreArglistRefKinds,
mixableArguments,
escapeArguments);
foreach (var (parameter, argument, refKind) in escapeArguments)
{
// This means it's part of an __arglist or function pointer receiver.
if (parameter is null)
{
if (argument.Type?.IsRefLikeOrAllowsRefLikeType() == true)
{
escapeValues.Add(new EscapeValue(parameter: null, argument, EscapeLevel.CallingMethod, isRefEscape: false));
}
continue;
}
if (parameter.Type.IsRefLikeOrAllowsRefLikeType())
{
escapeValues.Add(new EscapeValue(parameter, argument, EscapeLevel.CallingMethod, isRefEscape: false));
}
if (parameter.RefKind != RefKind.None)
{
escapeValues.Add(new EscapeValue(parameter, argument, EscapeLevel.CallingMethod, isRefEscape: true));
}
}
escapeArguments.Free();
}
private static string GetInvocationParameterName(ParameterSymbol? parameter)
{
if (parameter is null)
{
return "__arglist";
}
string parameterName = parameter.Name;
if (string.IsNullOrEmpty(parameterName))
{
parameterName = parameter.Ordinal.ToString();
}
return parameterName;
}
private static void ReportInvocationEscapeError(
SyntaxNode syntax,
Symbol symbol,
ParameterSymbol? parameter,
bool checkingReceiver,
BindingDiagnosticBag diagnostics)
{
ErrorCode errorCode = GetStandardCallEscapeError(checkingReceiver);
string parameterName = GetInvocationParameterName(parameter);
Error(diagnostics, errorCode, syntax, symbol, parameterName);
}
private bool ShouldInferDeclarationExpressionValEscape(BoundExpression argument, [NotNullWhen(true)] out SourceLocalSymbol? localSymbol)
{
var symbol = argument switch
{
BoundDeconstructValuePlaceholder p => p.VariableSymbol,
BoundLocal { DeclarationKind: not BoundLocalDeclarationKind.None } l => l.LocalSymbol,
_ => null
};
if (symbol is SourceLocalSymbol local &&
GetLocalScopes(local).ValEscapeScope == CallingMethodScope)
{
localSymbol = local;
return true;
}
else
{
// No need to infer a val escape for a global variable.
// These are only used in top-level statements in scripting mode,
// and since they are class fields, their scope is always CallingMethod.
Debug.Assert(symbol is null or SourceLocalSymbol or GlobalExpressionVariable);
localSymbol = null;
return false;
}
}
/// <summary>
/// Validates whether the invocation is valid per no-mixing rules.
/// Returns <see langword="false"/> when it is not valid and produces diagnostics (possibly more than one recursively) that helps to figure the reason.
/// </summary>
private bool CheckInvocationArgMixing(
SyntaxNode syntax,
in MethodInfo methodInfo,
BoundExpression? receiverOpt,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
uint scopeOfTheContainingExpression,
BindingDiagnosticBag diagnostics)
{
if (methodInfo.UseUpdatedEscapeRules)
{
return CheckInvocationArgMixingWithUpdatedRules(syntax, methodInfo, receiverOpt, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, scopeOfTheContainingExpression, diagnostics);
}
// SPEC:
// In a method invocation, the following constraints apply:
// - If there is a ref or out argument of a ref struct type (including the receiver), with safe-to-escape E1, then
// - no argument (including the receiver) may have a narrower safe-to-escape than E1.
var symbol = methodInfo.Symbol;
if (!symbol.RequiresInstanceReceiver())
{
// ignore receiver when symbol is static
receiverOpt = null;
}
// widest possible escape via writeable ref-like receiver or ref/out argument.
uint escapeTo = scopeOfTheContainingExpression;
// collect all writeable ref-like arguments, including receiver
var escapeArguments = ArrayBuilder<EscapeArgument>.GetInstance();
GetInvocationArgumentsForEscape(
methodInfo,
receiverOpt,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt: default,
argsToParamsOpt,
ignoreArglistRefKinds: false,
mixableArguments: null,
escapeArguments);
try
{
foreach (var (_, argument, refKind) in escapeArguments)
{
if (ShouldInferDeclarationExpressionValEscape(argument, out _))
{
// Any variable from a declaration expression is a valid mixing destination as we
// infer a legal value escape for it. It does not contribute input as it's declared
// at this point (functions like an `out` in the new escape rules)
continue;
}
if (refKind.IsWritableReference()
&& !argument.IsDiscardExpression()
&& argument.Type?.IsRefLikeOrAllowsRefLikeType() == true)
{
escapeTo = Math.Min(escapeTo, GetValEscape(argument, scopeOfTheContainingExpression));
}
}
var hasMixingError = false;
// track the widest scope that arguments could safely escape to.
// use this scope as the inferred STE of declaration expressions.
var inferredDestinationValEscape = CallingMethodScope;
foreach (var (parameter, argument, _) in escapeArguments)
{
// in the old rules, we assume that refs cannot escape into ref struct variables.
// e.g. in `dest = M(ref arg)`, we assume `ref arg` will not escape into `dest`, but `arg` might.
inferredDestinationValEscape = Math.Max(inferredDestinationValEscape, GetValEscape(argument, scopeOfTheContainingExpression));
if (!hasMixingError && !CheckValEscape(argument.Syntax, argument, scopeOfTheContainingExpression, escapeTo, false, diagnostics))
{
string parameterName = GetInvocationParameterName(parameter);
Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, symbol, parameterName);
hasMixingError = true;
}
}
foreach (var (_, argument, _) in escapeArguments)
{
if (ShouldInferDeclarationExpressionValEscape(argument, out var localSymbol))
{
SetLocalScopes(localSymbol, refEscapeScope: _localScopeDepth, valEscapeScope: inferredDestinationValEscape);
}
}
return !hasMixingError;
}
finally
{
escapeArguments.Free();
}
}
private bool CheckInvocationArgMixingWithUpdatedRules(
SyntaxNode syntax,
in MethodInfo methodInfo,
BoundExpression? receiverOpt,
ThreeState receiverIsSubjectToCloning,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<RefKind> argRefKindsOpt,
ImmutableArray<int> argsToParamsOpt,
uint scopeOfTheContainingExpression,
BindingDiagnosticBag diagnostics)
{
var mixableArguments = ArrayBuilder<MixableDestination>.GetInstance();
var escapeValues = ArrayBuilder<EscapeValue>.GetInstance();
GetEscapeValuesForUpdatedRules(
methodInfo,
receiverOpt,
receiverIsSubjectToCloning,
parameters,
argsOpt,
argRefKindsOpt,
argsToParamsOpt,
ignoreArglistRefKinds: false,
mixableArguments,
escapeValues);
var valid = true;
foreach (var mixableArg in mixableArguments)
{
var toArgEscape = GetValEscape(mixableArg.Argument, scopeOfTheContainingExpression);
foreach (var (fromParameter, fromArg, escapeKind, isRefEscape) in escapeValues)
{
// This checks to see if the EscapeValue could ever be assigned to this argument based
// on comparing the EscapeLevel of both. If this could never be assigned due to
// this then we don't need to consider it for MAMM analysis.
if (!mixableArg.IsAssignableFrom(escapeKind))
{
continue;
}
valid = isRefEscape
? CheckRefEscape(fromArg.Syntax, fromArg, scopeOfTheContainingExpression, toArgEscape, checkingReceiver: false, diagnostics)
: CheckValEscape(fromArg.Syntax, fromArg, scopeOfTheContainingExpression, toArgEscape, checkingReceiver: false, diagnostics);
if (!valid)
{
string parameterName = GetInvocationParameterName(fromParameter);
Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, methodInfo.Symbol, parameterName);
break;
}
}
if (!valid)
{
break;
}
}
inferDeclarationExpressionValEscape();
mixableArguments.Free();
escapeValues.Free();
return valid;
void inferDeclarationExpressionValEscape()
{
// find the widest scope that arguments could safely escape to.
// use this scope as the inferred STE of declaration expressions.
var inferredDestinationValEscape = CallingMethodScope;
foreach (var (_, fromArg, _, isRefEscape) in escapeValues)
{
inferredDestinationValEscape = Math.Max(inferredDestinationValEscape, isRefEscape
? GetRefEscape(fromArg, scopeOfTheContainingExpression)
: GetValEscape(fromArg, scopeOfTheContainingExpression));
}
foreach (var argument in argsOpt)
{
if (ShouldInferDeclarationExpressionValEscape(argument, out var localSymbol))
{
SetLocalScopes(localSymbol, refEscapeScope: _localScopeDepth, valEscapeScope: inferredDestinationValEscape);
}
}
}
}
#if DEBUG
private static bool AllParametersConsideredInEscapeAnalysisHaveArguments(
ImmutableArray<BoundExpression> argsOpt,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<int> argsToParamsOpt)
{
if (parameters.IsDefaultOrEmpty) return true;
var paramsMatched = BitVector.Create(parameters.Length);
for (int argIndex = 0; argIndex < argsOpt.Length; argIndex++)
{
int paramIndex = argsToParamsOpt.IsDefault ? argIndex : argsToParamsOpt[argIndex];
paramsMatched[paramIndex] = true;
}
for (int paramIndex = 0; paramIndex < parameters.Length; paramIndex++)
{
if (!paramsMatched[paramIndex])
{
return false;
}
}
return true;
}
#endif
private static ErrorCode GetStandardCallEscapeError(bool checkingReceiver)
{
return checkingReceiver ? ErrorCode.ERR_EscapeCall2 : ErrorCode.ERR_EscapeCall;
}
private sealed class TypeParameterThisParameterSymbol : ThisParameterSymbolBase
{
private readonly TypeParameterSymbol _type;
private readonly ParameterSymbol _underlyingParameter;
internal TypeParameterThisParameterSymbol(ParameterSymbol underlyingParameter, TypeParameterSymbol type)
{
Debug.Assert(underlyingParameter.IsThis);
Debug.Assert(underlyingParameter.RefKind != RefKind.Out); // Shouldn't get here for a constructor
Debug.Assert(underlyingParameter.ContainingSymbol is MethodSymbol);
_underlyingParameter = underlyingParameter;
_type = type;
}
public override TypeWithAnnotations TypeWithAnnotations
=> TypeWithAnnotations.Create(_type, NullableAnnotation.NotAnnotated);
public override RefKind RefKind
{
get
{
if (_underlyingParameter.RefKind is not RefKind.None and var underlyingRefKind)
{
return underlyingRefKind;
}
if (!_underlyingParameter.ContainingType.IsInterface || _type.IsReferenceType)
{
return RefKind.None;
}
// Receiver of an interface method could possibly be a structure.
// Let's treat it as by ref parameter for the purpose of ref safety analysis.
return RefKind.Ref;
}
}
public override ImmutableArray<Location> Locations
{
get { return _underlyingParameter.Locations; }
}
public override Symbol ContainingSymbol
{
get { return _underlyingParameter.ContainingSymbol; }
}
internal override ScopedKind EffectiveScope
{
get
{
if (HasUnscopedRefAttribute)
{
return ScopedKind.None;
}
if (!_underlyingParameter.ContainingType.IsInterface || _type.IsReferenceType)
{
return ScopedKind.None;
}
// Receiver of an interface method could possibly be a structure.
// Let's treat it as scoped ref by ref parameter for the purpose of ref safety analysis.
return ScopedKind.ScopedRef;
}
}
internal override bool HasUnscopedRefAttribute
=> _underlyingParameter.HasUnscopedRefAttribute;
internal sealed override bool UseUpdatedEscapeRules
=> _underlyingParameter.UseUpdatedEscapeRules;
}
#nullable disable
}
internal partial class Binder
{
private static void ReportReadonlyLocalError(SyntaxNode node, LocalSymbol local, BindValueKind kind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
Debug.Assert((object)local != null);
Debug.Assert(kind != BindValueKind.RValue);
MessageID cause;
if (local.IsForEach)
{
cause = MessageID.IDS_FOREACHLOCAL;
}
else if (local.IsUsing)
{
cause = MessageID.IDS_USINGLOCAL;
}
else if (local.IsFixed)
{
cause = MessageID.IDS_FIXEDLOCAL;
}
else
{
Error(diagnostics, GetStandardLvalueError(kind), node);
return;
}
ErrorCode[] ReadOnlyLocalErrors =
{
ErrorCode.ERR_RefReadonlyLocalCause,
ErrorCode.ERR_AssgReadonlyLocalCause,
ErrorCode.ERR_RefReadonlyLocal2Cause,
ErrorCode.ERR_AssgReadonlyLocal2Cause
};
int index = (checkingReceiver ? 2 : 0) + (RequiresRefOrOut(kind) ? 0 : 1);
Error(diagnostics, ReadOnlyLocalErrors[index], node, local, cause.Localize());
}
private static ErrorCode GetThisLvalueError(BindValueKind kind, bool isValueType, bool isPrimaryConstructorParameter)
{
switch (kind)
{
case BindValueKind.CompoundAssignment:
case BindValueKind.Assignable:
return ErrorCode.ERR_AssgReadonlyLocal;
case BindValueKind.RefOrOut:
return ErrorCode.ERR_RefReadonlyLocal;
case BindValueKind.AddressOf:
return ErrorCode.ERR_InvalidAddrOp;
case BindValueKind.IncrementDecrement:
return isValueType ? ErrorCode.ERR_AssgReadonlyLocal : ErrorCode.ERR_IncrementLvalueExpected;
case BindValueKind.RefReturn:
case BindValueKind.ReadonlyRef:
return isPrimaryConstructorParameter ? ErrorCode.ERR_RefReturnPrimaryConstructorParameter : ErrorCode.ERR_RefReturnThis;
case BindValueKind.RefAssignable:
return ErrorCode.ERR_RefLocalOrParamExpected;
}
if (RequiresReferenceToLocation(kind))
{
return ErrorCode.ERR_RefLvalueExpected;
}
throw ExceptionUtilities.UnexpectedValue(kind);
}
private static ErrorCode GetRangeLvalueError(BindValueKind kind)
{
switch (kind)
{
case BindValueKind.Assignable:
case BindValueKind.CompoundAssignment:
case BindValueKind.IncrementDecrement:
return ErrorCode.ERR_QueryRangeVariableReadOnly;
case BindValueKind.AddressOf:
return ErrorCode.ERR_InvalidAddrOp;
case BindValueKind.RefReturn:
case BindValueKind.ReadonlyRef:
return ErrorCode.ERR_RefReturnRangeVariable;
case BindValueKind.RefAssignable:
return ErrorCode.ERR_RefLocalOrParamExpected;
}
if (RequiresReferenceToLocation(kind))
{
return ErrorCode.ERR_QueryOutRefRangeVariable;
}
throw ExceptionUtilities.UnexpectedValue(kind);
}
private static ErrorCode GetMethodGroupOrFunctionPointerLvalueError(BindValueKind valueKind)
{
if (RequiresReferenceToLocation(valueKind))
{
return ErrorCode.ERR_RefReadonlyLocalCause;
}
// Cannot assign to 'W' because it is a 'method group'
return ErrorCode.ERR_AssgReadonlyLocalCause;
}
private static ErrorCode GetStandardLvalueError(BindValueKind kind)
{
switch (kind)
{
case BindValueKind.CompoundAssignment:
case BindValueKind.Assignable:
return ErrorCode.ERR_AssgLvalueExpected;
case BindValueKind.AddressOf:
return ErrorCode.ERR_InvalidAddrOp;
case BindValueKind.IncrementDecrement:
return ErrorCode.ERR_IncrementLvalueExpected;
case BindValueKind.FixedReceiver:
return ErrorCode.ERR_FixedNeedsLvalue;
case BindValueKind.RefReturn:
case BindValueKind.ReadonlyRef:
return ErrorCode.ERR_RefReturnLvalueExpected;
case BindValueKind.RefAssignable:
return ErrorCode.ERR_RefLocalOrParamExpected;
}
if (RequiresReferenceToLocation(kind))
{
return ErrorCode.ERR_RefLvalueExpected;
}
throw ExceptionUtilities.UnexpectedValue(kind);
}
}
internal partial class RefSafetyAnalysis
{
private static ErrorCode GetStandardRValueRefEscapeError(uint escapeTo)
{
if (escapeTo is CallingMethodScope or ReturnOnlyScope)
{
return ErrorCode.ERR_RefReturnLvalueExpected;
}
return ErrorCode.ERR_EscapeOther;
}
}
internal partial class Binder
{
private static void ReportReadOnlyFieldError(FieldSymbol field, SyntaxNode node, BindValueKind kind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
Debug.Assert((object)field != null);
Debug.Assert(field.RefKind == RefKind.None ? RequiresAssignableVariable(kind) : RequiresRefAssignableVariable(kind));
Debug.Assert(field.Type != (object)null);
// It's clearer to say that the address can't be taken than to say that the field can't be modified
// (even though the latter message gives more explanation of why).
Debug.Assert(kind != BindValueKind.AddressOf); // If this assert fails, we probably should report ErrorCode.ERR_InvalidAddrOp
ErrorCode[] ReadOnlyErrors =
{
ErrorCode.ERR_RefReturnReadonly,
ErrorCode.ERR_RefReadonly,
ErrorCode.ERR_AssgReadonly,
ErrorCode.ERR_RefReturnReadonlyStatic,
ErrorCode.ERR_RefReadonlyStatic,
ErrorCode.ERR_AssgReadonlyStatic,
ErrorCode.ERR_RefReturnReadonly2,
ErrorCode.ERR_RefReadonly2,
ErrorCode.ERR_AssgReadonly2,
ErrorCode.ERR_RefReturnReadonlyStatic2,
ErrorCode.ERR_RefReadonlyStatic2,
ErrorCode.ERR_AssgReadonlyStatic2
};
int index = (checkingReceiver ? 6 : 0) + (field.IsStatic ? 3 : 0) + (kind == BindValueKind.RefReturn ? 0 : (RequiresRefOrOut(kind) ? 1 : 2));
if (checkingReceiver)
{
Error(diagnostics, ReadOnlyErrors[index], node, field);
}
else
{
Error(diagnostics, ReadOnlyErrors[index], node);
}
}
private static void ReportReadOnlyError(Symbol symbol, SyntaxNode node, BindValueKind kind, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
Debug.Assert((object)symbol != null);
Debug.Assert(RequiresAssignableVariable(kind));
// It's clearer to say that the address can't be taken than to say that the parameter can't be modified
// (even though the latter message gives more explanation of why).
if (kind == BindValueKind.AddressOf)
{
Error(diagnostics, ErrorCode.ERR_InvalidAddrOp, node);
return;
}
var symbolKind = symbol.Kind.Localize();
ErrorCode[] ReadOnlyErrors =
{
ErrorCode.ERR_RefReturnReadonlyNotField,
ErrorCode.ERR_RefReadonlyNotField,
ErrorCode.ERR_AssignReadonlyNotField,
ErrorCode.ERR_RefReturnReadonlyNotField2,
ErrorCode.ERR_RefReadonlyNotField2,
ErrorCode.ERR_AssignReadonlyNotField2,
};
int index = (checkingReceiver ? 3 : 0) + (kind == BindValueKind.RefReturn ? 0 : (RequiresRefOrOut(kind) ? 1 : 2));
Error(diagnostics, ReadOnlyErrors[index], node, symbolKind, new FormattedSymbol(symbol, SymbolDisplayFormat.ShortFormat));
}
}
internal partial class RefSafetyAnalysis
{
/// <summary>
/// Checks whether given expression can escape from the current scope to the <paramref name="escapeTo"/>.
/// </summary>
internal void ValidateEscape(BoundExpression expr, uint escapeTo, bool isByRef, BindingDiagnosticBag diagnostics)
{
// The result of escape analysis is affected by the expression's type.
// We can't do escape analysis on expressions which lack a type, such as 'target typed new()', until they are converted.
Debug.Assert(expr.Type is not null);
if (isByRef)
{
CheckRefEscape(expr.Syntax, expr, _localScopeDepth, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
}
else
{
CheckValEscape(expr.Syntax, expr, _localScopeDepth, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
}
}
/// <summary>
/// Computes the widest scope depth to which the given expression can escape by reference.
///
/// NOTE: in a case if expression cannot be passed by an alias (RValue and similar), the ref-escape is scopeOfTheContainingExpression
/// There are few cases where RValues are permitted to be passed by reference which implies that a temporary local proxy is passed instead.
/// We reflect such behavior by constraining the escape value to the narrowest scope possible.
/// </summary>
internal uint GetRefEscape(BoundExpression expr, uint scopeOfTheContainingExpression)
{
#if DEBUG
AssertVisited(expr);
#endif
// cannot infer anything from errors
if (expr.HasAnyErrors)
{
return CallingMethodScope;
}
// cannot infer anything from Void (broken code)
if (expr.Type?.GetSpecialTypeSafe() == SpecialType.System_Void)
{
return CallingMethodScope;
}
// constants/literals cannot ref-escape current scope
if (expr.ConstantValueOpt != null)
{
return scopeOfTheContainingExpression;
}
// cover case that cannot refer to local state
// otherwise default to current scope (RValues, etc)
switch (expr.Kind)
{
case BoundKind.ArrayAccess:
case BoundKind.PointerIndirectionOperator:
case BoundKind.PointerElementAccess:
// array elements and pointer dereferencing are readwrite variables
return CallingMethodScope;
case BoundKind.RefValueOperator:
// The undocumented __refvalue(tr, T) expression results in an lvalue of type T.
// for compat reasons it is not ref-returnable (since TypedReference is not val-returnable)
// it can, however, ref-escape to any other level (since TypedReference can val-escape to any other level)
return CurrentMethodScope;
case BoundKind.DiscardExpression:
// same as write-only byval local
break;
case BoundKind.DynamicMemberAccess:
case BoundKind.DynamicIndexerAccess:
// dynamic expressions can be read and written to
// can even be passed by reference (which is implemented via a temp)
// it is not valid to escape them by reference though, so treat them as RValues here
break;
case BoundKind.Parameter:
return GetParameterRefEscape(((BoundParameter)expr).ParameterSymbol);
case BoundKind.Local:
return GetLocalScopes(((BoundLocal)expr).LocalSymbol).RefEscapeScope;
case BoundKind.CapturedReceiverPlaceholder:
// Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration
return ((BoundCapturedReceiverPlaceholder)expr).LocalScopeDepth;
case BoundKind.ThisReference:
var thisParam = ((MethodSymbol)_symbol).ThisParameter;
Debug.Assert(thisParam.Type.Equals(((BoundThisReference)expr).Type, TypeCompareKind.ConsiderEverything));
return GetParameterRefEscape(thisParam);
case BoundKind.ConditionalOperator:
var conditional = (BoundConditionalOperator)expr;
if (conditional.IsRef)
{
// ref conditional defers to its operands
return Math.Max(GetRefEscape(conditional.Consequence, scopeOfTheContainingExpression),
GetRefEscape(conditional.Alternative, scopeOfTheContainingExpression));
}
// otherwise it is an RValue
break;
case BoundKind.FieldAccess:
return GetFieldRefEscape((BoundFieldAccess)expr, scopeOfTheContainingExpression);
case BoundKind.EventAccess:
var eventAccess = (BoundEventAccess)expr;
if (!eventAccess.IsUsableAsField)
{
// not field-like events are RValues
break;
}
var eventSymbol = eventAccess.EventSymbol;
// field-like events that are static or belong to reference types can ref escape anywhere
if (eventSymbol.IsStatic || eventSymbol.ContainingType.IsReferenceType)
{
return CallingMethodScope;
}
// for other events defer to the receiver.
return GetRefEscape(eventAccess.ReceiverOpt, scopeOfTheContainingExpression);
case BoundKind.Call:
{
var call = (BoundCall)expr;
var methodSymbol = call.Method;
if (methodSymbol.RefKind == RefKind.None)
{
break;
}
return GetInvocationEscapeScope(
MethodInfo.Create(call.Method),
call.ReceiverOpt,
call.InitialBindingReceiverIsSubjectToCloning,
methodSymbol.Parameters,
call.Arguments,
call.ArgumentRefKindsOpt,
call.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: true);
}
case BoundKind.FunctionPointerInvocation:
{
var ptrInvocation = (BoundFunctionPointerInvocation)expr;
var methodSymbol = ptrInvocation.FunctionPointer.Signature;
if (methodSymbol.RefKind == RefKind.None)
{
break;
}
return GetInvocationEscapeScope(
MethodInfo.Create(methodSymbol),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
methodSymbol.Parameters,
ptrInvocation.Arguments,
ptrInvocation.ArgumentRefKindsOpt,
argsToParamsOpt: default,
scopeOfTheContainingExpression,
isRefEscape: true);
}
case BoundKind.IndexerAccess:
{
var indexerAccess = (BoundIndexerAccess)expr;
var indexerSymbol = indexerAccess.Indexer;
return GetInvocationEscapeScope(
MethodInfo.Create(indexerAccess),
indexerAccess.ReceiverOpt,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerSymbol.Parameters,
indexerAccess.Arguments,
indexerAccess.ArgumentRefKindsOpt,
indexerAccess.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: true);
}
case BoundKind.ImplicitIndexerAccess:
var implicitIndexerAccess = (BoundImplicitIndexerAccess)expr;
// Note: the Argument and LengthOrCountAccess use is purely local
switch (implicitIndexerAccess.IndexerOrSliceAccess)
{
case BoundIndexerAccess indexerAccess:
var indexerSymbol = indexerAccess.Indexer;
return GetInvocationEscapeScope(
MethodInfo.Create(indexerAccess),
implicitIndexerAccess.Receiver,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerSymbol.Parameters,
indexerAccess.Arguments,
indexerAccess.ArgumentRefKindsOpt,
indexerAccess.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: true);
case BoundArrayAccess:
// array elements are readwrite variables
return CallingMethodScope;
case BoundCall call:
var methodSymbol = call.Method;
if (methodSymbol.RefKind == RefKind.None)
{
break;
}
return GetInvocationEscapeScope(
MethodInfo.Create(call.Method),
implicitIndexerAccess.Receiver,
call.InitialBindingReceiverIsSubjectToCloning,
methodSymbol.Parameters,
call.Arguments,
call.ArgumentRefKindsOpt,
call.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: true);
default:
throw ExceptionUtilities.UnexpectedValue(implicitIndexerAccess.IndexerOrSliceAccess.Kind);
}
break;
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)expr;
if (elementAccess.GetItemOrSliceHelper is not (WellKnownMember.System_ReadOnlySpan_T__get_Item or WellKnownMember.System_Span_T__get_Item) || elementAccess.IsValue)
{
Debug.Assert(GetInlineArrayAccessEquivalentSignatureMethod(elementAccess, out _, out _).RefKind == RefKind.None);
break;
}
ImmutableArray<BoundExpression> arguments;
ImmutableArray<RefKind> refKinds;
SignatureOnlyMethodSymbol equivalentSignatureMethod = GetInlineArrayAccessEquivalentSignatureMethod(elementAccess, out arguments, out refKinds);
Debug.Assert(equivalentSignatureMethod.RefKind != RefKind.None);
return GetInvocationEscapeScope(
MethodInfo.Create(equivalentSignatureMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
equivalentSignatureMethod.Parameters,
arguments,
refKinds,
argsToParamsOpt: default,
scopeOfTheContainingExpression,
isRefEscape: true);
}
case BoundKind.PropertyAccess:
var propertyAccess = (BoundPropertyAccess)expr;
// not passing any arguments/parameters
return GetInvocationEscapeScope(
MethodInfo.Create(propertyAccess.PropertySymbol),
propertyAccess.ReceiverOpt,
propertyAccess.InitialBindingReceiverIsSubjectToCloning,
default,
default,
default,
default,
scopeOfTheContainingExpression,
isRefEscape: true);
case BoundKind.AssignmentOperator:
var assignment = (BoundAssignmentOperator)expr;
if (!assignment.IsRef)
{
// non-ref assignments are RValues
break;
}
return GetRefEscape(assignment.Left, scopeOfTheContainingExpression);
case BoundKind.Conversion:
Debug.Assert(expr is BoundConversion conversion &&
(!conversion.Conversion.IsUserDefined ||
conversion.Conversion.Method.HasUnsupportedMetadata ||
conversion.Conversion.Method.RefKind == RefKind.None));
break;
case BoundKind.UnaryOperator:
Debug.Assert(expr is BoundUnaryOperator unaryOperator &&
(unaryOperator.MethodOpt is not { } unaryMethod ||
unaryMethod.HasUnsupportedMetadata ||
unaryMethod.RefKind == RefKind.None));
break;
case BoundKind.BinaryOperator:
Debug.Assert(expr is BoundBinaryOperator binaryOperator &&
(binaryOperator.Method is not { } binaryMethod ||
binaryMethod.HasUnsupportedMetadata ||
binaryMethod.RefKind == RefKind.None));
break;
case BoundKind.UserDefinedConditionalLogicalOperator:
Debug.Assert(expr is BoundUserDefinedConditionalLogicalOperator logicalOperator &&
(logicalOperator.LogicalOperator.HasUnsupportedMetadata ||
logicalOperator.LogicalOperator.RefKind == RefKind.None));
break;
case BoundKind.CompoundAssignmentOperator:
Debug.Assert(expr is BoundCompoundAssignmentOperator compoundAssignmentOperator &&
(compoundAssignmentOperator.Operator.Method is not { } compoundMethod ||
compoundMethod.HasUnsupportedMetadata ||
compoundMethod.RefKind == RefKind.None));
break;
}
// At this point we should have covered all the possible cases for anything that is not a strict RValue.
return scopeOfTheContainingExpression;
}
/// <summary>
/// A counterpart to the GetRefEscape, which validates if given escape demand can be met by the expression.
/// The result indicates whether the escape is possible.
/// Additionally, the method emits diagnostics (possibly more than one, recursively) that would help identify the cause for the failure.
/// </summary>
internal bool CheckRefEscape(SyntaxNode node, BoundExpression expr, uint escapeFrom, uint escapeTo, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
#if DEBUG
AssertVisited(expr);
#endif
Debug.Assert(!checkingReceiver || expr.Type.IsValueType || expr.Type.IsTypeParameter());
if (escapeTo >= escapeFrom)
{
// escaping to same or narrower scope is ok.
return true;
}
if (expr.HasAnyErrors)
{
// already an error
return true;
}
// void references cannot escape (error should be reported somewhere)
if (expr.Type?.GetSpecialTypeSafe() == SpecialType.System_Void)
{
return true;
}
// references to constants/literals cannot escape higher.
if (expr.ConstantValueOpt != null)
{
Error(diagnostics, GetStandardRValueRefEscapeError(escapeTo), node);
return false;
}
switch (expr.Kind)
{
case BoundKind.ArrayAccess:
case BoundKind.PointerIndirectionOperator:
case BoundKind.PointerElementAccess:
// array elements and pointer dereferencing are readwrite variables
return true;
case BoundKind.RefValueOperator:
// The undocumented __refvalue(tr, T) expression results in an lvalue of type T.
// for compat reasons it is not ref-returnable (since TypedReference is not val-returnable)
if (escapeTo is CallingMethodScope or ReturnOnlyScope)
{
break;
}
// it can, however, ref-escape to any other level (since TypedReference can val-escape to any other level)
return true;
case BoundKind.DiscardExpression:
// same as write-only byval local
break;
case BoundKind.DynamicMemberAccess:
case BoundKind.DynamicIndexerAccess:
// dynamic expressions can be read and written to
// can even be passed by reference (which is implemented via a temp)
// it is not valid to escape them by reference though.
break;
case BoundKind.Parameter:
var parameter = (BoundParameter)expr;
return CheckParameterRefEscape(node, parameter, parameter.ParameterSymbol, escapeTo, checkingReceiver, diagnostics);
case BoundKind.Local:
var local = (BoundLocal)expr;
return CheckLocalRefEscape(node, local, escapeTo, checkingReceiver, diagnostics);
case BoundKind.CapturedReceiverPlaceholder:
// Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration
if (((BoundCapturedReceiverPlaceholder)expr).LocalScopeDepth <= escapeTo)
{
return true;
}
break;
case BoundKind.ThisReference:
var thisParam = ((MethodSymbol)_symbol).ThisParameter;
Debug.Assert(thisParam.Type.Equals(((BoundThisReference)expr).Type, TypeCompareKind.ConsiderEverything));
return CheckParameterRefEscape(node, expr, thisParam, escapeTo, checkingReceiver, diagnostics);
case BoundKind.ConditionalOperator:
var conditional = (BoundConditionalOperator)expr;
if (conditional.IsRef)
{
return CheckRefEscape(conditional.Consequence.Syntax, conditional.Consequence, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics) &&
CheckRefEscape(conditional.Alternative.Syntax, conditional.Alternative, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
}
// report standard lvalue error
break;
case BoundKind.FieldAccess:
var fieldAccess = (BoundFieldAccess)expr;
return CheckFieldRefEscape(node, fieldAccess, escapeFrom, escapeTo, diagnostics);
case BoundKind.EventAccess:
var eventAccess = (BoundEventAccess)expr;
if (!eventAccess.IsUsableAsField)
{
// not field-like events are RValues
break;
}
return CheckFieldLikeEventRefEscape(node, eventAccess, escapeFrom, escapeTo, diagnostics);
case BoundKind.Call:
{
var call = (BoundCall)expr;
var methodSymbol = call.Method;
if (methodSymbol.RefKind == RefKind.None)
{
break;
}
return CheckInvocationEscape(
call.Syntax,
MethodInfo.Create(methodSymbol),
call.ReceiverOpt,
call.InitialBindingReceiverIsSubjectToCloning,
methodSymbol.Parameters,
call.Arguments,
call.ArgumentRefKindsOpt,
call.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: true);
}
case BoundKind.IndexerAccess:
{
var indexerAccess = (BoundIndexerAccess)expr;
var indexerSymbol = indexerAccess.Indexer;
if (indexerSymbol.RefKind == RefKind.None)
{
break;
}
return CheckInvocationEscape(
indexerAccess.Syntax,
MethodInfo.Create(indexerAccess),
indexerAccess.ReceiverOpt,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerSymbol.Parameters,
indexerAccess.Arguments,
indexerAccess.ArgumentRefKindsOpt,
indexerAccess.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: true);
}
case BoundKind.ImplicitIndexerAccess:
var implicitIndexerAccess = (BoundImplicitIndexerAccess)expr;
// Note: the Argument and LengthOrCountAccess use is purely local
switch (implicitIndexerAccess.IndexerOrSliceAccess)
{
case BoundIndexerAccess indexerAccess:
var indexerSymbol = indexerAccess.Indexer;
if (indexerSymbol.RefKind == RefKind.None)
{
break;
}
return CheckInvocationEscape(
indexerAccess.Syntax,
MethodInfo.Create(indexerAccess),
implicitIndexerAccess.Receiver,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerSymbol.Parameters,
indexerAccess.Arguments,
indexerAccess.ArgumentRefKindsOpt,
indexerAccess.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: true);
case BoundArrayAccess:
// array elements are readwrite variables
return true;
case BoundCall call:
var methodSymbol = call.Method;
if (methodSymbol.RefKind == RefKind.None)
{
break;
}
return CheckInvocationEscape(
call.Syntax,
MethodInfo.Create(methodSymbol),
implicitIndexerAccess.Receiver,
call.InitialBindingReceiverIsSubjectToCloning,
methodSymbol.Parameters,
call.Arguments,
call.ArgumentRefKindsOpt,
call.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: true);
default:
throw ExceptionUtilities.UnexpectedValue(implicitIndexerAccess.IndexerOrSliceAccess.Kind);
}
break;
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)expr;
if (elementAccess.GetItemOrSliceHelper is not (WellKnownMember.System_ReadOnlySpan_T__get_Item or WellKnownMember.System_Span_T__get_Item) || elementAccess.IsValue)
{
Debug.Assert(GetInlineArrayAccessEquivalentSignatureMethod(elementAccess, out _, out _).RefKind == RefKind.None);
break;
}
ImmutableArray<BoundExpression> arguments;
ImmutableArray<RefKind> refKinds;
SignatureOnlyMethodSymbol equivalentSignatureMethod = GetInlineArrayAccessEquivalentSignatureMethod(elementAccess, out arguments, out refKinds);
Debug.Assert(equivalentSignatureMethod.RefKind != RefKind.None);
return CheckInvocationEscape(
elementAccess.Syntax,
MethodInfo.Create(equivalentSignatureMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
equivalentSignatureMethod.Parameters,
argsOpt: arguments,
argRefKindsOpt: refKinds,
argsToParamsOpt: default,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: true);
}
case BoundKind.FunctionPointerInvocation:
var functionPointerInvocation = (BoundFunctionPointerInvocation)expr;
FunctionPointerMethodSymbol signature = functionPointerInvocation.FunctionPointer.Signature;
if (signature.RefKind == RefKind.None)
{
break;
}
return CheckInvocationEscape(
functionPointerInvocation.Syntax,
MethodInfo.Create(signature),
functionPointerInvocation.InvokedExpression,
receiverIsSubjectToCloning: ThreeState.False,
signature.Parameters,
functionPointerInvocation.Arguments,
functionPointerInvocation.ArgumentRefKindsOpt,
argsToParamsOpt: default,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: true);
case BoundKind.PropertyAccess:
var propertyAccess = (BoundPropertyAccess)expr;
var propertySymbol = propertyAccess.PropertySymbol;
if (propertySymbol.RefKind == RefKind.None)
{
break;
}
// not passing any arguments/parameters
return CheckInvocationEscape(
propertyAccess.Syntax,
MethodInfo.Create(propertySymbol),
propertyAccess.ReceiverOpt,
propertyAccess.InitialBindingReceiverIsSubjectToCloning,
default,
default,
default,
default,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: true);
case BoundKind.AssignmentOperator:
var assignment = (BoundAssignmentOperator)expr;
// Only ref-assignments can be LValues
if (!assignment.IsRef)
{
break;
}
return CheckRefEscape(
node,
assignment.Left,
escapeFrom,
escapeTo,
checkingReceiver: false,
diagnostics);
case BoundKind.Conversion:
var conversion = (BoundConversion)expr;
if (conversion.Conversion == Conversion.ImplicitThrow)
{
return CheckRefEscape(node, conversion.Operand, escapeFrom, escapeTo, checkingReceiver, diagnostics);
}
Debug.Assert(!conversion.Conversion.IsUserDefined ||
conversion.Conversion.Method.HasUnsupportedMetadata ||
conversion.Conversion.Method.RefKind == RefKind.None);
break;
case BoundKind.UnaryOperator:
Debug.Assert(expr is BoundUnaryOperator unaryOperator &&
(unaryOperator.MethodOpt is not { } unaryMethod ||
unaryMethod.HasUnsupportedMetadata ||
unaryMethod.RefKind == RefKind.None));
break;
case BoundKind.BinaryOperator:
Debug.Assert(expr is BoundBinaryOperator binaryOperator &&
(binaryOperator.Method is not { } binaryMethod ||
binaryMethod.HasUnsupportedMetadata ||
binaryMethod.RefKind == RefKind.None));
break;
case BoundKind.UserDefinedConditionalLogicalOperator:
Debug.Assert(expr is BoundUserDefinedConditionalLogicalOperator logicalOperator &&
(logicalOperator.LogicalOperator.HasUnsupportedMetadata ||
logicalOperator.LogicalOperator.RefKind == RefKind.None));
break;
case BoundKind.CompoundAssignmentOperator:
Debug.Assert(expr is BoundCompoundAssignmentOperator compoundAssignmentOperator &&
(compoundAssignmentOperator.Operator.Method is not { } compoundMethod ||
compoundMethod.HasUnsupportedMetadata ||
compoundMethod.RefKind == RefKind.None));
break;
case BoundKind.ThrowExpression:
return true;
}
// At this point we should have covered all the possible cases for anything that is not a strict RValue.
Error(diagnostics, GetStandardRValueRefEscapeError(escapeTo), node);
return false;
}
internal uint GetBroadestValEscape(BoundTupleExpression expr, uint scopeOfTheContainingExpression)
{
uint broadest = scopeOfTheContainingExpression;
foreach (var element in expr.Arguments)
{
uint valEscape;
if (element is BoundTupleExpression te)
{
valEscape = GetBroadestValEscape(te, scopeOfTheContainingExpression);
}
else
{
valEscape = GetValEscape(element, scopeOfTheContainingExpression);
}
broadest = Math.Min(broadest, valEscape);
}
return broadest;
}
/// <summary>
/// Computes the widest scope depth to which the given expression can escape by value.
///
/// NOTE: unless the type of expression is ref-like, the result is Binder.ExternalScope since ordinary values can always be returned from methods.
/// </summary>
internal uint GetValEscape(BoundExpression expr, uint scopeOfTheContainingExpression)
{
#if DEBUG
AssertVisited(expr);
#endif
// cannot infer anything from errors
if (expr.HasAnyErrors)
{
return CallingMethodScope;
}
// constants/literals cannot refer to local state
if (expr.ConstantValueOpt != null)
{
return CallingMethodScope;
}
// to have local-referring values an expression must have a ref-like type
if (expr.Type?.IsRefLikeOrAllowsRefLikeType() != true)
{
return CallingMethodScope;
}
// cover case that can refer to local state
// otherwise default to ExternalScope (ordinary values)
switch (expr.Kind)
{
case BoundKind.ThisReference:
var thisParam = ((MethodSymbol)_symbol).ThisParameter;
Debug.Assert(thisParam.Type.Equals(((BoundThisReference)expr).Type, TypeCompareKind.ConsiderEverything));
return GetParameterValEscape(thisParam);
case BoundKind.DefaultLiteral:
case BoundKind.DefaultExpression:
case BoundKind.Utf8String:
// always returnable
return CallingMethodScope;
case BoundKind.Parameter:
return GetParameterValEscape(((BoundParameter)expr).ParameterSymbol);
case BoundKind.FromEndIndexExpression:
// We are going to call a constructor that takes an integer and a bool. Cannot leak any references through them.
// always returnable
return CallingMethodScope;
case BoundKind.TupleLiteral:
case BoundKind.ConvertedTupleLiteral:
var tupleLiteral = (BoundTupleExpression)expr;
return GetTupleValEscape(tupleLiteral.Arguments, scopeOfTheContainingExpression);
case BoundKind.MakeRefOperator:
case BoundKind.RefValueOperator:
// for compat reasons
// NB: it also means can`t assign stackalloc spans to a __refvalue
// we are ok with that.
return CallingMethodScope;
case BoundKind.DiscardExpression:
return CallingMethodScope;
case BoundKind.DeconstructValuePlaceholder:
case BoundKind.InterpolatedStringArgumentPlaceholder:
case BoundKind.AwaitableValuePlaceholder:
return GetPlaceholderScope((BoundValuePlaceholderBase)expr);
case BoundKind.Local:
return GetLocalScopes(((BoundLocal)expr).LocalSymbol).ValEscapeScope;
case BoundKind.CapturedReceiverPlaceholder:
// Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration
var placeholder = (BoundCapturedReceiverPlaceholder)expr;
return GetValEscape(placeholder.Receiver, placeholder.LocalScopeDepth);
case BoundKind.StackAllocArrayCreation:
case BoundKind.ConvertedStackAllocExpression:
return CurrentMethodScope;
case BoundKind.ConditionalOperator:
var conditional = (BoundConditionalOperator)expr;
var consEscape = GetValEscape(conditional.Consequence, scopeOfTheContainingExpression);
if (conditional.IsRef)
{
// ref conditional defers to one operand.
// the other one is the same or we will be reporting errors anyways.
return consEscape;
}
// val conditional gets narrowest of its operands
return Math.Max(consEscape,
GetValEscape(conditional.Alternative, scopeOfTheContainingExpression));
case BoundKind.NullCoalescingOperator:
var coalescingOp = (BoundNullCoalescingOperator)expr;
return Math.Max(GetValEscape(coalescingOp.LeftOperand, scopeOfTheContainingExpression),
GetValEscape(coalescingOp.RightOperand, scopeOfTheContainingExpression));
case BoundKind.FieldAccess:
var fieldAccess = (BoundFieldAccess)expr;
var fieldSymbol = fieldAccess.FieldSymbol;
if (fieldSymbol.IsStatic || !fieldSymbol.ContainingType.IsRefLikeType)
{
// Already an error state.
return CallingMethodScope;
}
// for ref-like fields defer to the receiver.
return GetValEscape(fieldAccess.ReceiverOpt, scopeOfTheContainingExpression);
case BoundKind.Call:
{
var call = (BoundCall)expr;
return GetInvocationEscapeScope(
MethodInfo.Create(call.Method),
call.ReceiverOpt,
call.InitialBindingReceiverIsSubjectToCloning,
call.Method.Parameters,
call.Arguments,
call.ArgumentRefKindsOpt,
call.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: false);
}
case BoundKind.FunctionPointerInvocation:
var ptrInvocation = (BoundFunctionPointerInvocation)expr;
var ptrSymbol = ptrInvocation.FunctionPointer.Signature;
return GetInvocationEscapeScope(
MethodInfo.Create(ptrSymbol),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
ptrSymbol.Parameters,
ptrInvocation.Arguments,
ptrInvocation.ArgumentRefKindsOpt,
argsToParamsOpt: default,
scopeOfTheContainingExpression,
isRefEscape: false);
case BoundKind.IndexerAccess:
{
var indexerAccess = (BoundIndexerAccess)expr;
var indexerSymbol = indexerAccess.Indexer;
return GetInvocationEscapeScope(
MethodInfo.Create(indexerAccess),
indexerAccess.ReceiverOpt,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerSymbol.Parameters,
indexerAccess.Arguments,
indexerAccess.ArgumentRefKindsOpt,
indexerAccess.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: false);
}
case BoundKind.ImplicitIndexerAccess:
var implicitIndexerAccess = (BoundImplicitIndexerAccess)expr;
// Note: the Argument and LengthOrCountAccess use is purely local
switch (implicitIndexerAccess.IndexerOrSliceAccess)
{
case BoundIndexerAccess indexerAccess:
var indexerSymbol = indexerAccess.Indexer;
return GetInvocationEscapeScope(
MethodInfo.Create(indexerAccess),
implicitIndexerAccess.Receiver,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerSymbol.Parameters,
indexerAccess.Arguments,
indexerAccess.ArgumentRefKindsOpt,
indexerAccess.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: false);
case BoundArrayAccess:
// only possible in error cases (if possible at all)
return scopeOfTheContainingExpression;
case BoundCall call:
return GetInvocationEscapeScope(
MethodInfo.Create(call.Method),
implicitIndexerAccess.Receiver,
call.InitialBindingReceiverIsSubjectToCloning,
call.Method.Parameters,
call.Arguments,
call.ArgumentRefKindsOpt,
call.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: false);
default:
throw ExceptionUtilities.UnexpectedValue(implicitIndexerAccess.IndexerOrSliceAccess.Kind);
}
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)expr;
ImmutableArray<BoundExpression> arguments;
ImmutableArray<RefKind> refKinds;
SignatureOnlyMethodSymbol equivalentSignatureMethod = GetInlineArrayAccessEquivalentSignatureMethod(elementAccess, out arguments, out refKinds);
return GetInvocationEscapeScope(
MethodInfo.Create(equivalentSignatureMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
equivalentSignatureMethod.Parameters,
arguments,
refKinds,
argsToParamsOpt: default,
scopeOfTheContainingExpression,
isRefEscape: false);
}
case BoundKind.PropertyAccess:
var propertyAccess = (BoundPropertyAccess)expr;
// not passing any arguments/parameters
return GetInvocationEscapeScope(
MethodInfo.Create(propertyAccess.PropertySymbol),
propertyAccess.ReceiverOpt,
propertyAccess.InitialBindingReceiverIsSubjectToCloning,
default,
default,
default,
default,
scopeOfTheContainingExpression,
isRefEscape: false);
case BoundKind.ObjectCreationExpression:
{
var objectCreation = (BoundObjectCreationExpression)expr;
var constructorSymbol = objectCreation.Constructor;
var escape = GetInvocationEscapeScope(
MethodInfo.Create(constructorSymbol),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
constructorSymbol.Parameters,
objectCreation.Arguments,
objectCreation.ArgumentRefKindsOpt,
objectCreation.ArgsToParamsOpt,
scopeOfTheContainingExpression,
isRefEscape: false);
var initializerOpt = objectCreation.InitializerExpressionOpt;
if (initializerOpt != null)
{
escape = Math.Max(escape, GetValEscape(initializerOpt, scopeOfTheContainingExpression));
}
return escape;
}
case BoundKind.NewT:
{
var newT = (BoundNewT)expr;
// By default it is safe to escape
var escape = CallingMethodScope;
var initializerOpt = newT.InitializerExpressionOpt;
if (initializerOpt != null)
{
escape = Math.Max(escape, GetValEscape(initializerOpt, scopeOfTheContainingExpression));
}
return escape;
}
case BoundKind.WithExpression:
var withExpression = (BoundWithExpression)expr;
return Math.Max(GetValEscape(withExpression.Receiver, scopeOfTheContainingExpression),
GetValEscape(withExpression.InitializerExpression, scopeOfTheContainingExpression));
case BoundKind.UnaryOperator:
var unaryOperator = (BoundUnaryOperator)expr;
if (unaryOperator.MethodOpt is { } unaryMethod)
{
return GetInvocationEscapeScope(
MethodInfo.Create(unaryMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
unaryMethod.Parameters,
argsOpt: [unaryOperator.Operand],
argRefKindsOpt: default,
argsToParamsOpt: default,
scopeOfTheContainingExpression: scopeOfTheContainingExpression,
isRefEscape: false);
}
return GetValEscape(unaryOperator.Operand, scopeOfTheContainingExpression);
case BoundKind.Conversion:
var conversion = (BoundConversion)expr;
Debug.Assert(conversion.ConversionKind != ConversionKind.StackAllocToSpanType, "StackAllocToSpanType unexpected");
if (conversion.ConversionKind == ConversionKind.InterpolatedStringHandler)
{
return GetInterpolatedStringHandlerConversionEscapeScope(conversion.Operand, scopeOfTheContainingExpression);
}
if (conversion.ConversionKind == ConversionKind.CollectionExpression)
{
return HasLocalScope((BoundCollectionExpression)conversion.Operand) ?
CurrentMethodScope :
CallingMethodScope;
}
if (conversion.Conversion.IsInlineArray)
{
ImmutableArray<BoundExpression> arguments;
ImmutableArray<RefKind> refKinds;
SignatureOnlyMethodSymbol equivalentSignatureMethod = GetInlineArrayConversionEquivalentSignatureMethod(conversion, out arguments, out refKinds);
return GetInvocationEscapeScope(
MethodInfo.Create(equivalentSignatureMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
equivalentSignatureMethod.Parameters,
arguments,
refKinds,
argsToParamsOpt: default,
scopeOfTheContainingExpression,
isRefEscape: false);
}
if (conversion.Conversion.IsUserDefined)
{
var operatorMethod = conversion.Conversion.Method;
Debug.Assert(operatorMethod is not null);
return GetInvocationEscapeScope(
MethodInfo.Create(operatorMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
operatorMethod.Parameters,
argsOpt: [conversion.Operand],
argRefKindsOpt: default,
argsToParamsOpt: default,
scopeOfTheContainingExpression: scopeOfTheContainingExpression,
isRefEscape: false);
}
return GetValEscape(conversion.Operand, scopeOfTheContainingExpression);
case BoundKind.AssignmentOperator:
// https://github.com/dotnet/roslyn/issues/73549:
// We do not have a test that demonstrates that the statement below makes a difference.
// If 'scopeOfTheContainingExpression' is always returned, not a single test fails.
// Same for the 'case BoundKind.NullCoalescingAssignmentOperator:' below.
return GetValEscape(((BoundAssignmentOperator)expr).Right, scopeOfTheContainingExpression);
case BoundKind.NullCoalescingAssignmentOperator:
return GetValEscape(((BoundNullCoalescingAssignmentOperator)expr).RightOperand, scopeOfTheContainingExpression);
case BoundKind.IncrementOperator:
return GetValEscape(((BoundIncrementOperator)expr).Operand, scopeOfTheContainingExpression);
case BoundKind.CompoundAssignmentOperator:
var compound = (BoundCompoundAssignmentOperator)expr;
if (compound.Operator.Method is { } compoundMethod)
{
return GetInvocationEscapeScope(
MethodInfo.Create(compoundMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
compoundMethod.Parameters,
argsOpt: [compound.Left, compound.Right],
argRefKindsOpt: default,
argsToParamsOpt: default,
scopeOfTheContainingExpression: scopeOfTheContainingExpression,
isRefEscape: false);
}
return Math.Max(GetValEscape(compound.Left, scopeOfTheContainingExpression),
GetValEscape(compound.Right, scopeOfTheContainingExpression));
case BoundKind.BinaryOperator:
var binary = (BoundBinaryOperator)expr;
if (binary.Method is { } binaryMethod)
{
return GetInvocationEscapeScope(
MethodInfo.Create(binaryMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
binaryMethod.Parameters,
argsOpt: [binary.Left, binary.Right],
argRefKindsOpt: default,
argsToParamsOpt: default,
scopeOfTheContainingExpression: scopeOfTheContainingExpression,
isRefEscape: false);
}
return Math.Max(GetValEscape(binary.Left, scopeOfTheContainingExpression),
GetValEscape(binary.Right, scopeOfTheContainingExpression));
case BoundKind.RangeExpression:
var range = (BoundRangeExpression)expr;
return Math.Max((range.LeftOperandOpt is { } left ? GetValEscape(left, scopeOfTheContainingExpression) : CallingMethodScope),
(range.RightOperandOpt is { } right ? GetValEscape(right, scopeOfTheContainingExpression) : CallingMethodScope));
case BoundKind.UserDefinedConditionalLogicalOperator:
var uo = (BoundUserDefinedConditionalLogicalOperator)expr;
return GetInvocationEscapeScope(
MethodInfo.Create(uo.LogicalOperator),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
uo.LogicalOperator.Parameters,
argsOpt: [uo.Left, uo.Right],
argRefKindsOpt: default,
argsToParamsOpt: default,
scopeOfTheContainingExpression: scopeOfTheContainingExpression,
isRefEscape: false);
case BoundKind.QueryClause:
return GetValEscape(((BoundQueryClause)expr).Value, scopeOfTheContainingExpression);
case BoundKind.RangeVariable:
return GetValEscape(((BoundRangeVariable)expr).Value, scopeOfTheContainingExpression);
case BoundKind.ObjectInitializerExpression:
var initExpr = (BoundObjectInitializerExpression)expr;
return GetValEscapeOfObjectInitializer(initExpr, scopeOfTheContainingExpression);
case BoundKind.CollectionInitializerExpression:
var colExpr = (BoundCollectionInitializerExpression)expr;
return GetValEscape(colExpr.Initializers, scopeOfTheContainingExpression);
case BoundKind.CollectionElementInitializer:
var colElement = (BoundCollectionElementInitializer)expr;
return GetValEscape(colElement.Arguments, scopeOfTheContainingExpression);
case BoundKind.ObjectInitializerMember:
// this node generally makes no sense outside of the context of containing initializer
// however binder uses it as a placeholder when binding assignments inside an object initializer
// just say it does not escape anywhere, so that we do not get false errors.
return scopeOfTheContainingExpression;
case BoundKind.ImplicitReceiver:
case BoundKind.ObjectOrCollectionValuePlaceholder:
// binder uses this as a placeholder when binding members inside an object initializer
// just say it does not escape anywhere, so that we do not get false errors.
return scopeOfTheContainingExpression;
case BoundKind.InterpolatedStringHandlerPlaceholder:
// The handler placeholder cannot escape out of the current expression, as it's a compiler-synthesized
// location.
return scopeOfTheContainingExpression;
case BoundKind.DisposableValuePlaceholder:
// Disposable value placeholder is only ever used to lookup a pattern dispose method
// then immediately discarded. The actual expression will be generated during lowering
return scopeOfTheContainingExpression;
case BoundKind.PointerElementAccess:
case BoundKind.PointerIndirectionOperator:
// Unsafe code will always be allowed to escape.
return CallingMethodScope;
case BoundKind.AsOperator:
case BoundKind.AwaitExpression:
case BoundKind.ConditionalAccess:
case BoundKind.ConditionalReceiver:
case BoundKind.ArrayAccess:
// only possible in error cases (if possible at all)
return scopeOfTheContainingExpression;
case BoundKind.ConvertedSwitchExpression:
case BoundKind.UnconvertedSwitchExpression:
var switchExpr = (BoundSwitchExpression)expr;
return GetValEscape(switchExpr.SwitchArms.SelectAsArray(a => a.Value), scopeOfTheContainingExpression);
default:
// in error situations some unexpected nodes could make here
// returning "scopeOfTheContainingExpression" seems safer than throwing.
// we will still assert to make sure that all nodes are accounted for.
RoslynDebug.Assert(false, $"{expr.Kind} expression of {expr.Type} type");
return scopeOfTheContainingExpression;
}
}
#nullable enable
private bool HasLocalScope(BoundCollectionExpression expr)
{
// A non-empty collection expression with span type may be stored
// on the stack. In those cases the expression may have local scope.
if (expr.Type?.IsRefLikeType != true || expr.Elements.Length == 0)
{
return false;
}
var collectionTypeKind = ConversionsBase.GetCollectionExpressionTypeKind(_compilation, expr.Type, out var elementType);
switch (collectionTypeKind)
{
case CollectionExpressionTypeKind.ReadOnlySpan:
Debug.Assert(elementType.Type is { });
return !LocalRewriter.ShouldUseRuntimeHelpersCreateSpan(expr, elementType.Type);
case CollectionExpressionTypeKind.Span:
return true;
case CollectionExpressionTypeKind.CollectionBuilder:
// For a ref struct type with a builder method, the scope of the collection
// expression is the scope of an invocation of the builder method with the
// collection expression as the span argument. That is, `R r = [x, y, z];`
// is equivalent to `R r = Builder.Create((ReadOnlySpan<...>)[x, y, z]);`.
var constructMethod = expr.CollectionBuilderMethod;
if (constructMethod is not { Parameters: [{ RefKind: RefKind.None } parameter] })
{
// Unexpected construct method. Restrict the collection to local scope.
return true;
}
Debug.Assert(constructMethod.ReturnType.Equals(expr.Type, TypeCompareKind.AllIgnoreOptions));
Debug.Assert(parameter.Type.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.AllIgnoreOptions));
if (parameter.EffectiveScope == ScopedKind.ScopedValue)
{
return false;
}
if (LocalRewriter.ShouldUseRuntimeHelpersCreateSpan(expr, ((NamedTypeSymbol)parameter.Type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type))
{
return false;
}
return true;
case CollectionExpressionTypeKind.ImplementsIEnumerable:
// Error cases. Restrict the collection to local scope.
return true;
default:
throw ExceptionUtilities.UnexpectedValue(collectionTypeKind); // ref struct collection type with unexpected type kind
}
}
private uint GetTupleValEscape(ImmutableArray<BoundExpression> elements, uint scopeOfTheContainingExpression)
{
uint narrowestScope = scopeOfTheContainingExpression;
foreach (var element in elements)
{
narrowestScope = Math.Max(narrowestScope, GetValEscape(element, scopeOfTheContainingExpression));
}
return narrowestScope;
}
/// <summary>
/// The escape value of an object initializer is calculated by looking at all of the
/// expressions that can be stored into the implicit receiver. That means arguments
/// passed to an indexer for example only matter if they can escape into the receiver
/// as a stored field.
/// </summary>
private uint GetValEscapeOfObjectInitializer(BoundObjectInitializerExpression initExpr, uint scopeOfTheContainingExpression)
{
var result = CallingMethodScope;
foreach (var expr in initExpr.Initializers)
{
var exprResult = GetValEscapeOfObjectMemberInitializer(expr, scopeOfTheContainingExpression);
result = Math.Max(result, exprResult);
}
return result;
}
private uint GetValEscapeOfObjectMemberInitializer(BoundExpression expr, uint scopeOfTheContainingExpression)
{
uint result;
if (expr.Kind == BoundKind.AssignmentOperator)
{
var assignment = (BoundAssignmentOperator)expr;
var rightEscape = assignment.IsRef
? GetRefEscape(assignment.Right, scopeOfTheContainingExpression)
: GetValEscape(assignment.Right, scopeOfTheContainingExpression);
var left = (BoundObjectInitializerMember)assignment.Left;
result = left.MemberSymbol switch
{
PropertySymbol { IsIndexer: true } indexer => getIndexerEscape(indexer, left, rightEscape),
PropertySymbol property => getPropertyEscape(property, rightEscape),
_ => rightEscape
};
}
else
{
result = GetValEscape(expr, scopeOfTheContainingExpression);
}
return result;
uint getIndexerEscape(
PropertySymbol indexer,
BoundObjectInitializerMember expr,
uint rightEscapeScope)
{
Debug.Assert(expr.AccessorKind != AccessorKind.Unknown);
var methodInfo = MethodInfo.Create(indexer, expr.AccessorKind);
if (methodInfo.Method is null)
{
return CallingMethodScope;
}
// If the indexer is readonly then none of the arguments can contribute to
// the receiver escape
if (methodInfo.Method.IsEffectivelyReadOnly)
{
return CallingMethodScope;
}
var escapeValues = ArrayBuilder<EscapeValue>.GetInstance();
GetEscapeValues(
methodInfo,
// This is calculating the actual receiver scope
null,
ThreeState.Unknown,
methodInfo.Method.Parameters,
expr.Arguments,
expr.ArgumentRefKindsOpt,
expr.ArgsToParamsOpt,
ignoreArglistRefKinds: true,
mixableArguments: null,
escapeValues);
uint receiverEscapeScope = CallingMethodScope;
foreach (var escapeValue in escapeValues)
{
// This is a call to an indexer so the ref escape scope can only impact the escape value if it
// can be assigned to `this`. Return Only can't do this.
if (escapeValue.IsRefEscape && escapeValue.EscapeLevel != EscapeLevel.CallingMethod)
{
continue;
}
uint escapeScope = escapeValue.IsRefEscape
? GetRefEscape(escapeValue.Argument, scopeOfTheContainingExpression)
: GetValEscape(escapeValue.Argument, scopeOfTheContainingExpression);
receiverEscapeScope = Math.Max(escapeScope, receiverEscapeScope);
}
escapeValues.Free();
return Math.Max(receiverEscapeScope, rightEscapeScope);
}
uint getPropertyEscape(
PropertySymbol property,
uint rightEscapeScope)
{
var accessorKind = property.RefKind == RefKind.None ? AccessorKind.Set : AccessorKind.Get;
var methodInfo = MethodInfo.Create(property, accessorKind);
if (methodInfo.Method is null || methodInfo.Method.IsEffectivelyReadOnly)
{
return CallingMethodScope;
}
return rightEscapeScope;
}
}
#nullable disable
private uint GetValEscape(ImmutableArray<BoundExpression> expressions, uint scopeOfTheContainingExpression)
{
var result = CallingMethodScope;
foreach (var expression in expressions)
{
result = Math.Max(result, GetValEscape(expression, scopeOfTheContainingExpression));
}
return result;
}
/// <summary>
/// A counterpart to the GetValEscape, which validates if given escape demand can be met by the expression.
/// The result indicates whether the escape is possible.
/// Additionally, the method emits diagnostics (possibly more than one, recursively) that would help identify the cause for the failure.
/// </summary>
internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, uint escapeFrom, uint escapeTo, bool checkingReceiver, BindingDiagnosticBag diagnostics)
{
#if DEBUG
AssertVisited(expr);
#endif
Debug.Assert(!checkingReceiver || expr.Type.IsValueType || expr.Type.IsTypeParameter());
if (escapeTo >= escapeFrom)
{
// escaping to same or narrower scope is ok.
return true;
}
// cannot infer anything from errors
if (expr.HasAnyErrors)
{
return true;
}
// constants/literals cannot refer to local state
if (expr.ConstantValueOpt != null)
{
return true;
}
// to have local-referring values an expression must have a ref-like type
if (expr.Type?.IsRefLikeOrAllowsRefLikeType() != true)
{
return true;
}
bool inUnsafeRegion = _inUnsafeRegion;
switch (expr.Kind)
{
case BoundKind.ThisReference:
var thisParam = ((MethodSymbol)_symbol).ThisParameter;
Debug.Assert(thisParam.Type.Equals(((BoundThisReference)expr).Type, TypeCompareKind.ConsiderEverything));
return CheckParameterValEscape(node, thisParam, escapeTo, diagnostics);
case BoundKind.DefaultLiteral:
case BoundKind.DefaultExpression:
case BoundKind.Utf8String:
// always returnable
return true;
case BoundKind.Parameter:
return CheckParameterValEscape(node, ((BoundParameter)expr).ParameterSymbol, escapeTo, diagnostics);
case BoundKind.TupleLiteral:
case BoundKind.ConvertedTupleLiteral:
var tupleLiteral = (BoundTupleExpression)expr;
return CheckTupleValEscape(tupleLiteral.Arguments, escapeFrom, escapeTo, diagnostics);
case BoundKind.MakeRefOperator:
case BoundKind.RefValueOperator:
// for compat reasons
return true;
case BoundKind.DiscardExpression:
// same as uninitialized local
return true;
case BoundKind.DeconstructValuePlaceholder:
case BoundKind.AwaitableValuePlaceholder:
case BoundKind.InterpolatedStringArgumentPlaceholder:
if (GetPlaceholderScope((BoundValuePlaceholderBase)expr) > escapeTo)
{
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_EscapeVariable : ErrorCode.ERR_EscapeVariable, node, expr.Syntax);
return inUnsafeRegion;
}
return true;
case BoundKind.Local:
var localSymbol = ((BoundLocal)expr).LocalSymbol;
if (GetLocalScopes(localSymbol).ValEscapeScope > escapeTo)
{
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_EscapeVariable : ErrorCode.ERR_EscapeVariable, node, localSymbol);
return inUnsafeRegion;
}
return true;
case BoundKind.CapturedReceiverPlaceholder:
// Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration
BoundExpression underlyingReceiver = ((BoundCapturedReceiverPlaceholder)expr).Receiver;
return CheckValEscape(underlyingReceiver.Syntax, underlyingReceiver, escapeFrom, escapeTo, checkingReceiver, diagnostics);
case BoundKind.StackAllocArrayCreation:
case BoundKind.ConvertedStackAllocExpression:
if (escapeTo < CurrentMethodScope)
{
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_EscapeStackAlloc : ErrorCode.ERR_EscapeStackAlloc, node, expr.Type);
return inUnsafeRegion;
}
return true;
case BoundKind.UnconvertedConditionalOperator:
{
var conditional = (BoundUnconvertedConditionalOperator)expr;
return
CheckValEscape(conditional.Consequence.Syntax, conditional.Consequence, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics) &&
CheckValEscape(conditional.Alternative.Syntax, conditional.Alternative, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
}
case BoundKind.ConditionalOperator:
{
var conditional = (BoundConditionalOperator)expr;
var consValid = CheckValEscape(conditional.Consequence.Syntax, conditional.Consequence, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
if (!consValid || conditional.IsRef)
{
// ref conditional defers to one operand.
// the other one is the same or we will be reporting errors anyways.
return consValid;
}
return CheckValEscape(conditional.Alternative.Syntax, conditional.Alternative, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
}
case BoundKind.NullCoalescingOperator:
var coalescingOp = (BoundNullCoalescingOperator)expr;
return CheckValEscape(coalescingOp.LeftOperand.Syntax, coalescingOp.LeftOperand, escapeFrom, escapeTo, checkingReceiver, diagnostics) &&
CheckValEscape(coalescingOp.RightOperand.Syntax, coalescingOp.RightOperand, escapeFrom, escapeTo, checkingReceiver, diagnostics);
case BoundKind.FieldAccess:
var fieldAccess = (BoundFieldAccess)expr;
var fieldSymbol = fieldAccess.FieldSymbol;
if (fieldSymbol.IsStatic || !fieldSymbol.ContainingType.IsRefLikeType)
{
// Already an error state.
return true;
}
// for ref-like fields defer to the receiver.
return CheckValEscape(node, fieldAccess.ReceiverOpt, escapeFrom, escapeTo, true, diagnostics);
case BoundKind.Call:
{
var call = (BoundCall)expr;
var methodSymbol = call.Method;
return CheckInvocationEscape(
call.Syntax,
MethodInfo.Create(methodSymbol),
call.ReceiverOpt,
call.InitialBindingReceiverIsSubjectToCloning,
methodSymbol.Parameters,
call.Arguments,
call.ArgumentRefKindsOpt,
call.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
}
case BoundKind.FunctionPointerInvocation:
var ptrInvocation = (BoundFunctionPointerInvocation)expr;
var ptrSymbol = ptrInvocation.FunctionPointer.Signature;
return CheckInvocationEscape(
ptrInvocation.Syntax,
MethodInfo.Create(ptrSymbol),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
ptrSymbol.Parameters,
ptrInvocation.Arguments,
ptrInvocation.ArgumentRefKindsOpt,
argsToParamsOpt: default,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
case BoundKind.IndexerAccess:
{
var indexerAccess = (BoundIndexerAccess)expr;
var indexerSymbol = indexerAccess.Indexer;
return CheckInvocationEscape(
indexerAccess.Syntax,
MethodInfo.Create(indexerAccess),
indexerAccess.ReceiverOpt,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerSymbol.Parameters,
indexerAccess.Arguments,
indexerAccess.ArgumentRefKindsOpt,
indexerAccess.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
}
case BoundKind.ImplicitIndexerAccess:
var implicitIndexerAccess = (BoundImplicitIndexerAccess)expr;
// Note: the Argument and LengthOrCountAccess use is purely local
switch (implicitIndexerAccess.IndexerOrSliceAccess)
{
case BoundIndexerAccess indexerAccess:
var indexerSymbol = indexerAccess.Indexer;
return CheckInvocationEscape(
indexerAccess.Syntax,
MethodInfo.Create(indexerAccess),
implicitIndexerAccess.Receiver,
indexerAccess.InitialBindingReceiverIsSubjectToCloning,
indexerSymbol.Parameters,
indexerAccess.Arguments,
indexerAccess.ArgumentRefKindsOpt,
indexerAccess.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
case BoundArrayAccess:
// only possible in error cases (if possible at all)
return false;
case BoundCall call:
var methodSymbol = call.Method;
return CheckInvocationEscape(
call.Syntax,
MethodInfo.Create(methodSymbol),
implicitIndexerAccess.Receiver,
call.InitialBindingReceiverIsSubjectToCloning,
methodSymbol.Parameters,
call.Arguments,
call.ArgumentRefKindsOpt,
call.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
default:
throw ExceptionUtilities.UnexpectedValue(implicitIndexerAccess.IndexerOrSliceAccess.Kind);
}
case BoundKind.InlineArrayAccess:
{
var elementAccess = (BoundInlineArrayAccess)expr;
ImmutableArray<BoundExpression> arguments;
ImmutableArray<RefKind> refKinds;
SignatureOnlyMethodSymbol equivalentSignatureMethod = GetInlineArrayAccessEquivalentSignatureMethod(elementAccess, out arguments, out refKinds);
return CheckInvocationEscape(
elementAccess.Syntax,
MethodInfo.Create(equivalentSignatureMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
equivalentSignatureMethod.Parameters,
argsOpt: arguments,
argRefKindsOpt: refKinds,
argsToParamsOpt: default,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
}
case BoundKind.PropertyAccess:
var propertyAccess = (BoundPropertyAccess)expr;
// not passing any arguments/parameters
return CheckInvocationEscape(
propertyAccess.Syntax,
MethodInfo.Create(propertyAccess.PropertySymbol),
propertyAccess.ReceiverOpt,
propertyAccess.InitialBindingReceiverIsSubjectToCloning,
default,
default,
default,
default,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
case BoundKind.ObjectCreationExpression:
{
var objectCreation = (BoundObjectCreationExpression)expr;
var constructorSymbol = objectCreation.Constructor;
var escape = CheckInvocationEscape(
objectCreation.Syntax,
MethodInfo.Create(constructorSymbol),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
constructorSymbol.Parameters,
objectCreation.Arguments,
objectCreation.ArgumentRefKindsOpt,
objectCreation.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
var initializerExpr = objectCreation.InitializerExpressionOpt;
if (initializerExpr != null)
{
escape = escape &&
CheckValEscape(
initializerExpr.Syntax,
initializerExpr,
escapeFrom,
escapeTo,
checkingReceiver: false,
diagnostics: diagnostics);
}
return escape;
}
case BoundKind.NewT:
{
var newT = (BoundNewT)expr;
var escape = true;
var initializerExpr = newT.InitializerExpressionOpt;
if (initializerExpr != null)
{
escape = escape &&
CheckValEscape(
initializerExpr.Syntax,
initializerExpr,
escapeFrom,
escapeTo,
checkingReceiver: false,
diagnostics: diagnostics);
}
return escape;
}
case BoundKind.WithExpression:
{
var withExpr = (BoundWithExpression)expr;
var escape = CheckValEscape(node, withExpr.Receiver, escapeFrom, escapeTo, checkingReceiver: false, diagnostics);
var initializerExpr = withExpr.InitializerExpression;
escape = escape && CheckValEscape(initializerExpr.Syntax, initializerExpr, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
return escape;
}
case BoundKind.UnaryOperator:
var unary = (BoundUnaryOperator)expr;
if (unary.MethodOpt is { } unaryMethod)
{
return CheckInvocationEscape(
unary.Syntax,
MethodInfo.Create(unaryMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
unaryMethod.Parameters,
argsOpt: [unary.Operand],
argRefKindsOpt: default,
argsToParamsOpt: default,
checkingReceiver: checkingReceiver,
escapeFrom: escapeFrom,
escapeTo: escapeTo,
diagnostics,
isRefEscape: false);
}
return CheckValEscape(node, unary.Operand, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.FromEndIndexExpression:
// We are going to call a constructor that takes an integer and a bool. Cannot leak any references through them.
return true;
case BoundKind.Conversion:
var conversion = (BoundConversion)expr;
Debug.Assert(conversion.ConversionKind != ConversionKind.StackAllocToSpanType, "StackAllocToSpanType unexpected");
if (conversion.ConversionKind == ConversionKind.InterpolatedStringHandler)
{
return CheckInterpolatedStringHandlerConversionEscape(conversion.Operand, escapeFrom, escapeTo, diagnostics);
}
if (conversion.ConversionKind == ConversionKind.CollectionExpression)
{
if (HasLocalScope((BoundCollectionExpression)conversion.Operand) && escapeTo < CurrentMethodScope)
{
Error(diagnostics, ErrorCode.ERR_CollectionExpressionEscape, node, expr.Type);
return false;
}
return true;
}
if (conversion.Conversion.IsInlineArray)
{
ImmutableArray<BoundExpression> arguments;
ImmutableArray<RefKind> refKinds;
SignatureOnlyMethodSymbol equivalentSignatureMethod = GetInlineArrayConversionEquivalentSignatureMethod(conversion, out arguments, out refKinds);
return CheckInvocationEscape(
conversion.Syntax,
MethodInfo.Create(equivalentSignatureMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
equivalentSignatureMethod.Parameters,
argsOpt: arguments,
argRefKindsOpt: refKinds,
argsToParamsOpt: default,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);
}
if (conversion.Conversion.IsUserDefined)
{
var operatorMethod = conversion.Conversion.Method;
Debug.Assert(operatorMethod is not null);
return CheckInvocationEscape(
conversion.Syntax,
MethodInfo.Create(operatorMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
operatorMethod.Parameters,
argsOpt: [conversion.Operand],
argRefKindsOpt: default,
argsToParamsOpt: default,
checkingReceiver: checkingReceiver,
escapeFrom: escapeFrom,
escapeTo: escapeTo,
diagnostics,
isRefEscape: false);
}
return CheckValEscape(node, conversion.Operand, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.AssignmentOperator:
var assignment = (BoundAssignmentOperator)expr;
return CheckValEscape(node, assignment.Left, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.NullCoalescingAssignmentOperator:
var nullCoalescingAssignment = (BoundNullCoalescingAssignmentOperator)expr;
return CheckValEscape(node, nullCoalescingAssignment.LeftOperand, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.IncrementOperator:
var increment = (BoundIncrementOperator)expr;
return CheckValEscape(node, increment.Operand, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.CompoundAssignmentOperator:
var compound = (BoundCompoundAssignmentOperator)expr;
if (compound.Operator.Method is { } compoundMethod)
{
return CheckInvocationEscape(
compound.Syntax,
MethodInfo.Create(compoundMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
compoundMethod.Parameters,
argsOpt: [compound.Left, compound.Right],
argRefKindsOpt: default,
argsToParamsOpt: default,
checkingReceiver: checkingReceiver,
escapeFrom: escapeFrom,
escapeTo: escapeTo,
diagnostics,
isRefEscape: false);
}
return CheckValEscape(compound.Left.Syntax, compound.Left, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics) &&
CheckValEscape(compound.Right.Syntax, compound.Right, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.BinaryOperator:
var binary = (BoundBinaryOperator)expr;
if (binary.OperatorKind == BinaryOperatorKind.Utf8Addition)
{
return true;
}
if (binary.Method is { } binaryMethod)
{
return CheckInvocationEscape(
binary.Syntax,
MethodInfo.Create(binaryMethod),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
binaryMethod.Parameters,
argsOpt: [binary.Left, binary.Right],
argRefKindsOpt: default,
argsToParamsOpt: default,
checkingReceiver: checkingReceiver,
escapeFrom: escapeFrom,
escapeTo: escapeTo,
diagnostics,
isRefEscape: false);
}
return CheckValEscape(binary.Left.Syntax, binary.Left, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics) &&
CheckValEscape(binary.Right.Syntax, binary.Right, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.RangeExpression:
var range = (BoundRangeExpression)expr;
if (range.LeftOperandOpt is { } left && !CheckValEscape(left.Syntax, left, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics))
{
return false;
}
return !(range.RightOperandOpt is { } right && !CheckValEscape(right.Syntax, right, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics));
case BoundKind.UserDefinedConditionalLogicalOperator:
var uo = (BoundUserDefinedConditionalLogicalOperator)expr;
return CheckInvocationEscape(
uo.Syntax,
MethodInfo.Create(uo.LogicalOperator),
receiver: null,
receiverIsSubjectToCloning: ThreeState.Unknown,
uo.LogicalOperator.Parameters,
argsOpt: [uo.Left, uo.Right],
argRefKindsOpt: default,
argsToParamsOpt: default,
checkingReceiver: checkingReceiver,
escapeFrom: escapeFrom,
escapeTo: escapeTo,
diagnostics,
isRefEscape: false);
case BoundKind.QueryClause:
var clauseValue = ((BoundQueryClause)expr).Value;
return CheckValEscape(clauseValue.Syntax, clauseValue, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.RangeVariable:
var variableValue = ((BoundRangeVariable)expr).Value;
return CheckValEscape(variableValue.Syntax, variableValue, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
case BoundKind.ObjectInitializerExpression:
var initExpr = (BoundObjectInitializerExpression)expr;
return CheckValEscapeOfObjectInitializer(initExpr, escapeFrom, escapeTo, diagnostics);
// this would be correct implementation for CollectionInitializerExpression
// however it is unclear if it is reachable since the initialized type must implement IEnumerable
case BoundKind.CollectionInitializerExpression:
var colExpr = (BoundCollectionInitializerExpression)expr;
return CheckValEscape(colExpr.Initializers, escapeFrom, escapeTo, diagnostics);
// this would be correct implementation for CollectionElementInitializer
// however it is unclear if it is reachable since the initialized type must implement IEnumerable
case BoundKind.CollectionElementInitializer:
var colElement = (BoundCollectionElementInitializer)expr;
return CheckValEscape(colElement.Arguments, escapeFrom, escapeTo, diagnostics);
case BoundKind.PointerElementAccess:
var accessedExpression = ((BoundPointerElementAccess)expr).Expression;
return CheckValEscape(accessedExpression.Syntax, accessedExpression, escapeFrom, escapeTo, checkingReceiver, diagnostics);
case BoundKind.PointerIndirectionOperator:
var operandExpression = ((BoundPointerIndirectionOperator)expr).Operand;
return CheckValEscape(operandExpression.Syntax, operandExpression, escapeFrom, escapeTo, checkingReceiver, diagnostics);
case BoundKind.AsOperator:
case BoundKind.AwaitExpression:
case BoundKind.ConditionalAccess:
case BoundKind.ConditionalReceiver:
case BoundKind.ArrayAccess:
// only possible in error cases (if possible at all)
return false;
case BoundKind.UnconvertedSwitchExpression:
case BoundKind.ConvertedSwitchExpression:
foreach (var arm in ((BoundSwitchExpression)expr).SwitchArms)
{
var result = arm.Value;
if (!CheckValEscape(result.Syntax, result, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics))
return false;
}
return true;
default:
// in error situations some unexpected nodes could make here
// returning "false" seems safer than throwing.
// we will still assert to make sure that all nodes are accounted for.
RoslynDebug.Assert(false, $"{expr.Kind} expression of {expr.Type} type");
diagnostics.Add(ErrorCode.ERR_InternalError, node.Location);
return false;
#region "cannot produce ref-like values"
// case BoundKind.ThrowExpression:
// case BoundKind.ArgListOperator:
// case BoundKind.ArgList:
// case BoundKind.RefTypeOperator:
// case BoundKind.AddressOfOperator:
// case BoundKind.TypeOfOperator:
// case BoundKind.IsOperator:
// case BoundKind.SizeOfOperator:
// case BoundKind.DynamicMemberAccess:
// case BoundKind.DynamicInvocation:
// case BoundKind.DelegateCreationExpression:
// case BoundKind.ArrayCreation:
// case BoundKind.AnonymousObjectCreationExpression:
// case BoundKind.NameOfOperator:
// case BoundKind.InterpolatedString:
// case BoundKind.StringInsert:
// case BoundKind.DynamicIndexerAccess:
// case BoundKind.Lambda:
// case BoundKind.DynamicObjectCreationExpression:
// case BoundKind.NoPiaObjectCreationExpression:
// case BoundKind.BaseReference:
// case BoundKind.Literal:
// case BoundKind.IsPatternExpression:
// case BoundKind.DeconstructionAssignmentOperator:
// case BoundKind.EventAccess:
#endregion
#region "not expression that can produce a value"
// case BoundKind.FieldEqualsValue:
// case BoundKind.PropertyEqualsValue:
// case BoundKind.ParameterEqualsValue:
// case BoundKind.NamespaceExpression:
// case BoundKind.TypeExpression:
// case BoundKind.BadStatement:
// case BoundKind.MethodDefIndex:
// case BoundKind.SourceDocumentIndex:
// case BoundKind.ArgList:
// case BoundKind.ArgListOperator:
// case BoundKind.Block:
// case BoundKind.Scope:
// case BoundKind.NoOpStatement:
// case BoundKind.ReturnStatement:
// case BoundKind.YieldReturnStatement:
// case BoundKind.YieldBreakStatement:
// case BoundKind.ThrowStatement:
// case BoundKind.ExpressionStatement:
// case BoundKind.SwitchStatement:
// case BoundKind.SwitchSection:
// case BoundKind.SwitchLabel:
// case BoundKind.BreakStatement:
// case BoundKind.LocalFunctionStatement:
// case BoundKind.ContinueStatement:
// case BoundKind.PatternSwitchStatement:
// case BoundKind.PatternSwitchSection:
// case BoundKind.PatternSwitchLabel:
// case BoundKind.IfStatement:
// case BoundKind.DoStatement:
// case BoundKind.WhileStatement:
// case BoundKind.ForStatement:
// case BoundKind.ForEachStatement:
// case BoundKind.ForEachDeconstructStep:
// case BoundKind.UsingStatement:
// case BoundKind.FixedStatement:
// case BoundKind.LockStatement:
// case BoundKind.TryStatement:
// case BoundKind.CatchBlock:
// case BoundKind.LabelStatement:
// case BoundKind.GotoStatement:
// case BoundKind.LabeledStatement:
// case BoundKind.Label:
// case BoundKind.StatementList:
// case BoundKind.ConditionalGoto:
// case BoundKind.LocalDeclaration:
// case BoundKind.MultipleLocalDeclarations:
// case BoundKind.ArrayInitialization:
// case BoundKind.AnonymousPropertyDeclaration:
// case BoundKind.MethodGroup:
// case BoundKind.PropertyGroup:
// case BoundKind.EventAssignmentOperator:
// case BoundKind.Attribute:
// case BoundKind.FixedLocalCollectionInitializer:
// case BoundKind.DynamicObjectInitializerMember:
// case BoundKind.DynamicCollectionElementInitializer:
// case BoundKind.ImplicitReceiver:
// case BoundKind.FieldInitializer:
// case BoundKind.GlobalStatementInitializer:
// case BoundKind.TypeOrInstanceInitializers:
// case BoundKind.DeclarationPattern:
// case BoundKind.ConstantPattern:
// case BoundKind.WildcardPattern:
#endregion
#region "not found as an operand in no-error unlowered bound tree"
// case BoundKind.MaximumMethodDefIndex:
// case BoundKind.InstrumentationPayloadRoot:
// case BoundKind.ModuleVersionId:
// case BoundKind.ModuleVersionIdString:
// case BoundKind.Dup:
// case BoundKind.TypeOrValueExpression:
// case BoundKind.BadExpression:
// case BoundKind.ArrayLength:
// case BoundKind.MethodInfo:
// case BoundKind.FieldInfo:
// case BoundKind.SequencePoint:
// case BoundKind.SequencePointExpression:
// case BoundKind.SequencePointWithSpan:
// case BoundKind.StateMachineScope:
// case BoundKind.ComplexConditionalReceiver:
// case BoundKind.PreviousSubmissionReference:
// case BoundKind.HostObjectMemberReference:
// case BoundKind.UnboundLambda:
// case BoundKind.LoweredConditionalAccess:
// case BoundKind.Sequence:
// case BoundKind.HoistedFieldAccess:
// case BoundKind.OutVariablePendingInference:
// case BoundKind.DeconstructionVariablePendingInference:
// case BoundKind.OutDeconstructVarPendingInference:
// case BoundKind.PseudoVariable:
#endregion
}
}
private SignatureOnlyMethodSymbol GetInlineArrayAccessEquivalentSignatureMethod(BoundInlineArrayAccess elementAccess, out ImmutableArray<BoundExpression> arguments, out ImmutableArray<RefKind> refKinds)
{
RefKind resultRefKind;
RefKind parameterRefKind;
if (elementAccess.GetItemOrSliceHelper is WellKnownMember.System_ReadOnlySpan_T__get_Item or WellKnownMember.System_Span_T__get_Item)
{
// inlineArray[index] is equivalent to calling a method with the signature:
// - ref T GetItem(ref inlineArray), or
// - ref readonly T GetItem(in inlineArray), or
// - T GetItem(inlineArray)
if (elementAccess.IsValue)
{
resultRefKind = RefKind.None;
parameterRefKind = RefKind.None;
}
else
{
resultRefKind = elementAccess.GetItemOrSliceHelper is WellKnownMember.System_ReadOnlySpan_T__get_Item ? RefKind.In : RefKind.Ref;
parameterRefKind = resultRefKind;
}
}
else if (elementAccess.GetItemOrSliceHelper is WellKnownMember.System_ReadOnlySpan_T__Slice_Int_Int or WellKnownMember.System_Span_T__Slice_Int_Int)
{
// inlineArray[Range] is equivalent to calling a method with the signature:
// - Span<T> Slice(ref inlineArray), or
// - ReadOnlySpan<T> Slice(in inlineArray)
resultRefKind = RefKind.None;
parameterRefKind = elementAccess.GetItemOrSliceHelper is WellKnownMember.System_ReadOnlySpan_T__Slice_Int_Int ? RefKind.In : RefKind.Ref;
}
else
{
throw ExceptionUtilities.Unreachable();
}
var equivalentSignatureMethod = new SignatureOnlyMethodSymbol(
name: "",
this._symbol.ContainingType,
MethodKind.Ordinary,
Cci.CallingConvention.Default,
ImmutableArray<TypeParameterSymbol>.Empty,
ImmutableArray.Create<ParameterSymbol>(new SignatureOnlyParameterSymbol(
TypeWithAnnotations.Create(elementAccess.Expression.Type),
ImmutableArray<CustomModifier>.Empty,
isParamsArray: false,
isParamsCollection: false,
parameterRefKind
)),
resultRefKind,
isInitOnly: false,
isStatic: true,
returnType: TypeWithAnnotations.Create(elementAccess.Type),
ImmutableArray<CustomModifier>.Empty,
ImmutableArray<MethodSymbol>.Empty);
arguments = ImmutableArray.Create(elementAccess.Expression);
refKinds = ImmutableArray.Create(parameterRefKind);
return equivalentSignatureMethod;
}
private SignatureOnlyMethodSymbol GetInlineArrayConversionEquivalentSignatureMethod(BoundConversion conversion, out ImmutableArray<BoundExpression> arguments, out ImmutableArray<RefKind> refKinds)
{
Debug.Assert(conversion.Conversion.IsInlineArray);
return GetInlineArrayConversionEquivalentSignatureMethod(inlineArray: conversion.Operand, resultType: conversion.Type, out arguments, out refKinds);
}
private SignatureOnlyMethodSymbol GetInlineArrayConversionEquivalentSignatureMethod(BoundExpression inlineArray, TypeSymbol resultType, out ImmutableArray<BoundExpression> arguments, out ImmutableArray<RefKind> refKinds)
{
// An inline array conversion is equivalent to calling a method with the signature:
// - Span<T> Convert(ref inlineArray), or
// - ReadOnlySpan<T> Convert(in inlineArray)
RefKind parameterRefKind = resultType.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.AllIgnoreOptions) ? RefKind.In : RefKind.Ref;
var equivalentSignatureMethod = new SignatureOnlyMethodSymbol(
name: "",
_symbol.ContainingType,
MethodKind.Ordinary,
Cci.CallingConvention.Default,
ImmutableArray<TypeParameterSymbol>.Empty,
ImmutableArray.Create<ParameterSymbol>(new SignatureOnlyParameterSymbol(
TypeWithAnnotations.Create(inlineArray.Type),
ImmutableArray<CustomModifier>.Empty,
isParamsArray: false,
isParamsCollection: false,
parameterRefKind
)),
RefKind.None,
isInitOnly: false,
isStatic: true,
returnType: TypeWithAnnotations.Create(resultType),
ImmutableArray<CustomModifier>.Empty,
ImmutableArray<MethodSymbol>.Empty);
arguments = ImmutableArray.Create(inlineArray);
refKinds = ImmutableArray.Create(parameterRefKind);
return equivalentSignatureMethod;
}
private bool CheckTupleValEscape(ImmutableArray<BoundExpression> elements, uint escapeFrom, uint escapeTo, BindingDiagnosticBag diagnostics)
{
foreach (var element in elements)
{
if (!CheckValEscape(element.Syntax, element, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics))
{
return false;
}
}
return true;
}
#nullable enable
private bool CheckValEscapeOfObjectInitializer(BoundObjectInitializerExpression initExpr, uint escapeFrom, uint escapeTo, BindingDiagnosticBag diagnostics)
{
foreach (var expr in initExpr.Initializers)
{
if (GetValEscapeOfObjectMemberInitializer(expr, escapeFrom) > escapeTo)
{
Error(diagnostics, _inUnsafeRegion ? ErrorCode.WRN_EscapeVariable : ErrorCode.ERR_EscapeVariable, initExpr.Syntax, expr.Syntax);
return false;
}
}
return true;
}
#nullable disable
private bool CheckValEscape(ImmutableArray<BoundExpression> expressions, uint escapeFrom, uint escapeTo, BindingDiagnosticBag diagnostics)
{
foreach (var expression in expressions)
{
if (!CheckValEscape(expression.Syntax, expression, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics))
{
return false;
}
}
return true;
}
private bool CheckInterpolatedStringHandlerConversionEscape(BoundExpression expression, uint escapeFrom, uint escapeTo, BindingDiagnosticBag diagnostics)
{
var data = expression.GetInterpolatedStringHandlerData();
// We need to check to see if any values could potentially escape outside the max depth via the handler type.
// Consider the case where a ref-struct handler saves off the result of one call to AppendFormatted,
// and then on a subsequent call it either assigns that saved value to another ref struct with a larger
// escape, or does the opposite. In either case, we need to check.
#if DEBUG
// VisitArgumentsAndGetArgumentPlaceholders() does not visit data.Construction
// since that expression does not introduce locals or placeholders that are needed
// by GetValEscape() or CheckValEscape(), so we disable tracking here.
var previousVisited = _visited;
_visited = null;
#endif
CheckValEscape(expression.Syntax, data.Construction, escapeFrom, escapeTo, checkingReceiver: false, diagnostics);
#if DEBUG
_visited = previousVisited;
#endif
var arguments = ArrayBuilder<BoundExpression>.GetInstance();
GetInterpolatedStringHandlerArgumentsForEscape(expression, arguments);
bool result = true;
foreach (var argument in arguments)
{
if (!CheckValEscape(argument.Syntax, argument, escapeFrom, escapeTo, checkingReceiver: false, diagnostics))
{
result = false;
break;
}
}
arguments.Free();
return result;
}
private void GetInterpolatedStringHandlerArgumentsForEscape(BoundExpression expression, ArrayBuilder<BoundExpression> arguments)
{
while (true)
{
switch (expression)
{
case BoundBinaryOperator binary:
GetInterpolatedStringHandlerArgumentsForEscape(binary.Right, arguments);
expression = binary.Left;
break;
case BoundInterpolatedString interpolatedString:
getParts(interpolatedString);
return;
default:
throw ExceptionUtilities.UnexpectedValue(expression.Kind);
}
}
void getParts(BoundInterpolatedString interpolatedString)
{
foreach (var part in interpolatedString.Parts)
{
if (part is not BoundCall { Method.Name: BoundInterpolatedString.AppendFormattedMethod } call)
{
// Dynamic calls cannot have ref struct parameters, and AppendLiteral calls will always have literal
// string arguments and do not require us to be concerned with escape
continue;
}
// The interpolation component is always the first argument to the method, and it was not passed by name
// so there can be no reordering.
// SPEC: For a given argument `a` that is passed to parameter `p`:
// SPEC: 1. ...
// SPEC: 2. If `p` is `scoped` then `a` does not contribute *safe-to-escape* when considering arguments.
if (_useUpdatedEscapeRules &&
call.Method.Parameters[0].EffectiveScope == ScopedKind.ScopedValue)
{
continue;
}
arguments.Add(call.Arguments[0]);
}
}
}
}
internal partial class Binder
{
internal enum AddressKind
{
// reference may be written to
Writeable,
// reference itself will not be written to, but may be used for call, callvirt.
// for all purposes it is the same as Writeable, except when fetching an address of an array element
// where it results in a ".readonly" prefix to deal with array covariance.
Constrained,
// reference itself will not be written to, nor it will be used to modify fields.
ReadOnly,
// same as ReadOnly, but we are not supposed to get a reference to a clone
// regardless of compat settings.
ReadOnlyStrict,
}
internal static bool IsAnyReadOnly(AddressKind addressKind) => addressKind >= AddressKind.ReadOnly;
/// <summary>
/// Checks if expression directly or indirectly represents a value with its own home. In
/// such cases it is possible to get a reference without loading into a temporary.
/// </summary>
internal static bool HasHome(
BoundExpression expression,
AddressKind addressKind,
Symbol containingSymbol,
bool peVerifyCompatEnabled,
HashSet<LocalSymbol> stackLocalsOpt)
{
Debug.Assert(containingSymbol is object);
switch (expression.Kind)
{
case BoundKind.ArrayAccess:
if (addressKind == AddressKind.ReadOnly && !expression.Type.IsValueType && peVerifyCompatEnabled)
{
// due to array covariance getting a reference may throw ArrayTypeMismatch when element is not a struct,
// passing "readonly." prefix would prevent that, but it is unverifiable, so will make a copy in compat case
return false;
}
return true;
case BoundKind.PointerIndirectionOperator:
case BoundKind.RefValueOperator:
return true;
case BoundKind.ThisReference:
var type = expression.Type;
if (type.IsReferenceType)
{
Debug.Assert(IsAnyReadOnly(addressKind), "`this` is readonly in classes");
return true;
}
if (!IsAnyReadOnly(addressKind) && containingSymbol is MethodSymbol { ContainingSymbol: NamedTypeSymbol, IsEffectivelyReadOnly: true })
{
return false;
}
return true;
case BoundKind.ThrowExpression:
// vacuously this is true, we can take address of throw without temps
return true;
case BoundKind.Parameter:
return IsAnyReadOnly(addressKind) ||
((BoundParameter)expression).ParameterSymbol.RefKind is not (RefKind.In or RefKind.RefReadOnlyParameter);
case BoundKind.Local:
// locals have home unless they are byval stack locals or ref-readonly
// locals in a mutating call
var local = ((BoundLocal)expression).LocalSymbol;
return !((CodeGenerator.IsStackLocal(local, stackLocalsOpt) && local.RefKind == RefKind.None) ||
(!IsAnyReadOnly(addressKind) && local.RefKind == RefKind.RefReadOnly));
case BoundKind.Call:
var methodRefKind = ((BoundCall)expression).Method.RefKind;
return methodRefKind == RefKind.Ref ||
(IsAnyReadOnly(addressKind) && methodRefKind == RefKind.RefReadOnly);
case BoundKind.Dup:
//NB: Dup represents locals that do not need IL slot
var dupRefKind = ((BoundDup)expression).RefKind;
return dupRefKind == RefKind.Ref ||
(IsAnyReadOnly(addressKind) && dupRefKind == RefKind.RefReadOnly);
case BoundKind.FieldAccess:
return FieldAccessHasHome((BoundFieldAccess)expression, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt);
case BoundKind.Sequence:
return HasHome(((BoundSequence)expression).Value, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt);
case BoundKind.AssignmentOperator:
var assignment = (BoundAssignmentOperator)expression;
if (!assignment.IsRef)
{
return false;
}
var lhsRefKind = assignment.Left.GetRefKind();
return lhsRefKind == RefKind.Ref ||
(IsAnyReadOnly(addressKind) && lhsRefKind is RefKind.RefReadOnly or RefKind.RefReadOnlyParameter);
case BoundKind.ComplexConditionalReceiver:
Debug.Assert(HasHome(
((BoundComplexConditionalReceiver)expression).ValueTypeReceiver,
addressKind,
containingSymbol,
peVerifyCompatEnabled,
stackLocalsOpt));
Debug.Assert(HasHome(
((BoundComplexConditionalReceiver)expression).ReferenceTypeReceiver,
addressKind,
containingSymbol,
peVerifyCompatEnabled,
stackLocalsOpt));
goto case BoundKind.ConditionalReceiver;
case BoundKind.ConditionalReceiver:
//ConditionalReceiver is a noop from Emit point of view. - it represents something that has already been pushed.
//We should never need a temp for it.
return true;
case BoundKind.ConditionalOperator:
var conditional = (BoundConditionalOperator)expression;
// only ref conditional may be referenced as a variable
if (!conditional.IsRef)
{
return false;
}
// branch that has no home will need a temporary
// if both have no home, just say whole expression has no home
// so we could just use one temp for the whole thing
return HasHome(conditional.Consequence, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt)
&& HasHome(conditional.Alternative, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt);
default:
return false;
}
}
/// <summary>
/// Special HasHome for fields.
/// A field has a readable home unless the field is a constant.
/// A ref readonly field doesn't have a writable home.
/// Other fields have a writable home unless the field is a readonly value
/// and is used outside of a constructor or init method.
/// </summary>
private static bool FieldAccessHasHome(
BoundFieldAccess fieldAccess,
AddressKind addressKind,
Symbol containingSymbol,
bool peVerifyCompatEnabled,
HashSet<LocalSymbol> stackLocalsOpt)
{
Debug.Assert(containingSymbol is object);
FieldSymbol field = fieldAccess.FieldSymbol;
// const fields are literal values with no homes. (ex: decimal.Zero)
if (field.IsConst)
{
return false;
}
if (field.RefKind is RefKind.Ref)
{
return true;
}
// in readonly situations where ref to a copy is not allowed, consider fields as addressable
if (addressKind == AddressKind.ReadOnlyStrict)
{
return true;
}
// ReadOnly references can always be taken unless we are in peverify compat mode
if (addressKind == AddressKind.ReadOnly && !peVerifyCompatEnabled)
{
return true;
}
// Some field accesses must be values; values do not have homes.
if (fieldAccess.IsByValue)
{
return false;
}
if (field.RefKind == RefKind.RefReadOnly)
{
return false;
}
Debug.Assert(field.RefKind == RefKind.None);
if (!field.IsReadOnly)
{
// in a case if we have a writeable struct field with a receiver that only has a readable home we would need to pass it via a temp.
// it would be advantageous to make a temp for the field, not for the outer struct, since the field is smaller and we can get to is by fetching references.
// NOTE: this would not be profitable if we have to satisfy verifier, since for verifiability
// we would not be able to dig for the inner field using references and the outer struct will have to be copied to a temp anyways.
if (!peVerifyCompatEnabled)
{
Debug.Assert(!IsAnyReadOnly(addressKind));
var receiver = fieldAccess.ReceiverOpt;
if (receiver?.Type.IsValueType == true)
{
// Check receiver:
// has writeable home -> return true - the whole chain has writeable home (also a more common case)
// has readable home -> return false - we need to copy the field
// otherwise -> return true - the copy will be made at higher level so the leaf field can have writeable home
return HasHome(receiver, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt)
|| !HasHome(receiver, AddressKind.ReadOnly, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt);
}
}
return true;
}
// while readonly fields have home it is not valid to refer to it when not constructing.
if (!TypeSymbol.Equals(field.ContainingType, containingSymbol.ContainingSymbol as NamedTypeSymbol, TypeCompareKind.AllIgnoreOptions))
{
return false;
}
if (field.IsStatic)
{
return containingSymbol is MethodSymbol { MethodKind: MethodKind.StaticConstructor } or FieldSymbol { IsStatic: true };
}
else
{
return (containingSymbol is MethodSymbol { MethodKind: MethodKind.Constructor } or FieldSymbol { IsStatic: false } or MethodSymbol { IsInitOnly: true }) &&
fieldAccess.ReceiverOpt.Kind == BoundKind.ThisReference;
}
}
}
}
|