Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
16 contributors

Users who have contributed to this file

@cston @333fred @jcouv @gafter @AlekseyTs @jaredpar @chsienki @agocke @VSadov @dotnet-bot @YairHalberstadt @filipw @Scott-Caldwell @RikkiGibson @JoeRobich @jasonmalinowski
6813 lines (6046 sloc) 305 KB
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DEBUG
// See comment in DefiniteAssignment.
#define REFERENCE_STATE
#endif
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp
{
/// <summary>
/// Nullability flow analysis.
/// </summary>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
internal sealed partial class NullableWalker : LocalDataFlowPass<NullableWalker.LocalState>
{
/// <summary>
/// Used to copy variable slots and types from the NullableWalker for the containing method
/// or lambda to the NullableWalker created for a nested lambda or local function.
/// </summary>
internal sealed class VariableState
{
// Consider referencing the collections directly from the original NullableWalker
// rather than copying the collections. (Items are added to the collections
// but never replaced so the collections are lazily populated but otherwise immutable.)
internal readonly ImmutableDictionary<VariableIdentifier, int> VariableSlot;
internal readonly ImmutableArray<VariableIdentifier> VariableBySlot;
internal readonly ImmutableDictionary<Symbol, TypeWithAnnotations> VariableTypes;
// The nullable state of all variables captured at the point where the function or lambda appeared.
internal readonly LocalState VariableNullableStates;
internal VariableState(
ImmutableDictionary<VariableIdentifier, int> variableSlot,
ImmutableArray<VariableIdentifier> variableBySlot,
ImmutableDictionary<Symbol, TypeWithAnnotations> variableTypes,
LocalState variableNullableStates)
{
VariableSlot = variableSlot;
VariableBySlot = variableBySlot;
VariableTypes = variableTypes;
VariableNullableStates = variableNullableStates;
}
}
/// <summary>
/// Represents the result of visiting an expression.
/// Contains a result type which tells us whether the expression may be null,
/// and an l-value type which tells us whether we can assign null to the expression.
/// </summary>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
private readonly struct VisitResult
{
public readonly TypeWithState RValueType;
public readonly TypeWithAnnotations LValueType;
public VisitResult(TypeWithState rValueType, TypeWithAnnotations lValueType)
{
RValueType = rValueType;
LValueType = lValueType;
// https://github.com/dotnet/roslyn/issues/34993: Doesn't hold true for Tuple_Assignment_10. See if we can make it hold true
//Debug.Assert((RValueType.Type is null && LValueType.TypeSymbol is null) ||
// RValueType.Type.Equals(LValueType.TypeSymbol, TypeCompareKind.ConsiderEverything | TypeCompareKind.AllIgnoreOptions));
}
public VisitResult(TypeSymbol type, NullableAnnotation annotation, NullableFlowState state)
{
RValueType = TypeWithState.Create(type, state);
LValueType = TypeWithAnnotations.Create(type, annotation);
Debug.Assert(RValueType.Type.Equals(LValueType.Type, TypeCompareKind.ConsiderEverything));
}
private string GetDebuggerDisplay() => $"{{LValue: {LValueType.GetDebuggerDisplay()}, RValue: {RValueType.GetDebuggerDisplay()}}}";
}
/// <summary>
/// Represents the result of visiting an argument expression.
/// In addition to storing the <see cref="VisitResult"/>, also stores the <see cref="LocalState"/>
/// for reanalyzing a lambda.
/// </summary>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
private readonly struct VisitArgumentResult
{
public readonly VisitResult VisitResult;
public readonly Optional<LocalState> StateForLambda;
public TypeWithState RValueType => VisitResult.RValueType;
public TypeWithAnnotations LValueType => VisitResult.LValueType;
public VisitArgumentResult(VisitResult visitResult, Optional<LocalState> stateForLambda)
{
VisitResult = visitResult;
StateForLambda = stateForLambda;
}
}
/// <summary>
/// The inferred type at the point of declaration of var locals and parameters.
/// </summary>
private readonly PooledDictionary<Symbol, TypeWithAnnotations> _variableTypes = PooledDictionary<Symbol, TypeWithAnnotations>.GetInstance();
/// <summary>
/// Binder for symbol being analyzed.
/// </summary>
private readonly Binder _binder;
/// <summary>
/// Conversions with nullability and unknown matching any.
/// </summary>
private readonly Conversions _conversions;
/// <summary>
/// Use the the parameter types and nullability from _methodSignatureOpt for initial
/// parameter state. If false, the signature of _member is used instead.
/// </summary>
private readonly bool _useMethodSignatureParameterTypes;
/// <summary>
/// Method signature used for return type or parameter types. Distinct from _member
/// signature when _member is a lambda and type is inferred from MethodTypeInferrer.
/// </summary>
private readonly MethodSymbol _methodSignatureOpt;
/// <summary>
/// Return statements and the result types from analyzing the returned expressions. Used when inferring lambda return type in MethodTypeInferrer.
/// </summary>
private readonly ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)> _returnTypesOpt;
/// <summary>
/// Invalid type, used only to catch Visit methods that do not set
/// _result.Type. See VisitExpressionWithoutStackGuard.
/// </summary>
private static readonly TypeWithState _invalidType = TypeWithState.Create(ErrorTypeSymbol.UnknownResultType, NullableFlowState.NotNull);
/// <summary>
/// Contains the map of expressions to inferred nullabilities and types used by the optional rewriter phase of the
/// compiler.
/// </summary>
private readonly Dictionary<BoundExpression, (NullabilityInfo Info, TypeSymbol Type)> _analyzedNullabilityMapOpt;
// https://github.com/dotnet/roslyn/issues/35043: remove this when all expression are supported
private bool _disableNullabilityAnalysis;
/// <summary>
/// State of method group receivers, used later when analyzing the conversion to a delegate.
/// (Could be replaced by _analyzedNullabilityMapOpt if that map is always available.)
/// </summary>
private PooledDictionary<BoundExpression, TypeWithState> _methodGroupReceiverMapOpt;
#if DEBUG
/// <summary>
/// Contains the expressions that should not be inserted into <see cref="_analyzedNullabilityMapOpt"/>.
/// </summary>
private static readonly ImmutableArray<BoundKind> s_skippedExpressions = ImmutableArray.Create(BoundKind.ArrayInitialization,
BoundKind.ObjectInitializerExpression,
BoundKind.CollectionInitializerExpression,
BoundKind.DynamicCollectionElementInitializer);
#endif
/// <summary>
/// The result and l-value type of the last visited expression.
/// </summary>
private VisitResult _visitResult;
/// <summary>
/// The visit result of the receiver for the current conditional access.
///
/// For example: A conditional invocation uses a placeholder as a receiver. By storing the
/// visit result from the actual receiver ahead of time, we can give this placeholder a correct result.
/// </summary>
private VisitResult _currentConditionalReceiverVisitResult;
/// <summary>
/// The result type represents the state of the last visited expression.
/// </summary>
private TypeWithState ResultType
{
get => _visitResult.RValueType;
}
private void SetResultType(BoundExpression expression, TypeWithState type)
{
SetResult(expression, resultType: type, lvalueType: type.ToTypeWithAnnotations());
}
/// <summary>
/// Force the inference of the LValueResultType from ResultType.
/// </summary>
private void UseRvalueOnly(BoundExpression expression)
{
SetResult(expression, ResultType, ResultType.ToTypeWithAnnotations(), isLvalue: false);
}
private TypeWithAnnotations LvalueResultType
{
get => _visitResult.LValueType;
}
private void SetLvalueResultType(BoundExpression expression, TypeWithAnnotations type)
{
SetResult(expression, resultType: type.ToTypeWithState(), lvalueType: type);
}
/// <summary>
/// Force the inference of the ResultType from LValueResultType.
/// </summary>
private void UseLvalueOnly(BoundExpression expression)
{
SetResult(expression, LvalueResultType.ToTypeWithState(), LvalueResultType, isLvalue: true);
}
private void SetInvalidResult() => SetResult(expression: null, _invalidType, _invalidType.ToTypeWithAnnotations(), updateAnalyzedNullability: false);
private void SetResult(BoundExpression expression, TypeWithState resultType, TypeWithAnnotations lvalueType, bool updateAnalyzedNullability = true, bool? isLvalue = null)
{
_visitResult = new VisitResult(resultType, lvalueType);
if (updateAnalyzedNullability)
{
SetAnalyzedNullability(expression, _visitResult, isLvalue);
}
}
private bool ShouldMakeNotNullRvalue(BoundExpression node) => node.IsSuppressed || node.HasAnyErrors || !IsReachable();
/// <summary>
/// Sets the analyzed nullability of the expression to be the given result.
/// </summary>
private void SetAnalyzedNullability(BoundExpression expr, VisitResult result, bool? isLvalue = null)
{
if (expr == null || _disableNullabilityAnalysis) return;
#if DEBUG
// https://github.com/dotnet/roslyn/issues/34993: This assert is essential for ensuring that we aren't
// changing the observable results of GetTypeInfo beyond nullability information.
//Debug.Assert(AreCloseEnough(expr.Type, result.RValueType.Type),
// $"Cannot change the type of {expr} from {expr.Type} to {result.RValueType.Type}");
#endif
if (_analyzedNullabilityMapOpt != null)
{
// https://github.com/dotnet/roslyn/issues/34993: enable and verify these assertions
#if false
if (_analyzedNullabilityMapOpt.TryGetValue(expr, out var existing))
{
if (!(result.RValueType.State == NullableFlowState.NotNull && ShouldMakeNotNullRvalue(expr, State.Reachable)))
{
switch (isLvalue)
{
case true:
Debug.Assert(existing.Info.Annotation == result.LValueType.NullableAnnotation.ToPublicAnnotation(),
$"Tried to update the nullability of {expr} from {existing.Info.Annotation} to {result.LValueType.NullableAnnotation}");
break;
case false:
Debug.Assert(existing.Info.FlowState == result.RValueType.State,
$"Tried to update the nullability of {expr} from {existing.Info.FlowState} to {result.RValueType.State}");
break;
case null:
Debug.Assert(existing.Info.Equals((NullabilityInfo)result),
$"Tried to update the nullability of {expr} from ({existing.Info.Annotation}, {existing.Info.FlowState}) to ({result.LValueType.NullableAnnotation}, {result.RValueType.State})");
break;
}
}
}
#endif
_analyzedNullabilityMapOpt[expr] = (new NullabilityInfo(result.LValueType.NullableAnnotation.ToPublicAnnotation(),
result.RValueType.State.ToPublicFlowState()),
// https://github.com/dotnet/roslyn/issues/35046 We're dropping the result if the type doesn't match up completely
// with the existing type
expr.Type?.Equals(result.RValueType.Type, TypeCompareKind.AllIgnoreOptions) == true ? result.RValueType.Type : expr.Type);
}
}
/// <summary>
/// Placeholder locals, e.g. for objects being constructed.
/// </summary>
private PooledDictionary<object, PlaceholderLocal> _placeholderLocalsOpt;
/// <summary>
/// For methods with annotations, we'll need to visit the arguments twice.
/// Once for diagnostics and once for result state (but disabling diagnostics).
/// </summary>
private bool _disableDiagnostics = false;
/// <summary>
/// Used to allow <see cref="MakeSlot(BoundExpression)"/> to substitute the correct slot for a <see cref="BoundConditionalReceiver"/> when
/// it's encountered.
/// </summary>
private int _lastConditionalAccessSlot = -1;
protected override void Free()
{
_methodGroupReceiverMapOpt?.Free();
_variableTypes.Free();
_placeholderLocalsOpt?.Free();
base.Free();
}
private NullableWalker(
CSharpCompilation compilation,
Symbol symbol,
bool useMethodSignatureParameterTypes,
MethodSymbol methodSignatureOpt,
BoundNode node,
Binder binder,
Conversions conversions,
ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)> returnTypesOpt,
VariableState initialState,
Dictionary<BoundExpression, (NullabilityInfo, TypeSymbol)> analyzedNullabilityMapOpt)
// Members of variables are tracked up to a fixed depth, to avoid cycles. The
// maxSlotDepth value is arbitrary but large enough to allow most scenarios.
: base(compilation, symbol, node, EmptyStructTypeCache.CreatePrecise(), trackUnassignments: true, maxSlotDepth: 5)
{
_binder = binder;
_conversions = (Conversions)conversions.WithNullability(true);
_useMethodSignatureParameterTypes = (object)methodSignatureOpt != null && useMethodSignatureParameterTypes;
_methodSignatureOpt = methodSignatureOpt;
_returnTypesOpt = returnTypesOpt;
_analyzedNullabilityMapOpt = analyzedNullabilityMapOpt;
if (initialState != null)
{
var variableBySlot = initialState.VariableBySlot;
nextVariableSlot = variableBySlot.Length;
foreach (var (variable, slot) in initialState.VariableSlot)
{
Debug.Assert(slot < nextVariableSlot);
_variableSlot.Add(variable, slot);
}
this.variableBySlot = variableBySlot.ToArray();
foreach (var pair in initialState.VariableTypes)
{
_variableTypes.Add(pair.Key, pair.Value);
}
this.State = initialState.VariableNullableStates.Clone();
}
}
public string GetDebuggerDisplay()
{
if (this.IsConditionalState)
{
return $"{{{GetType().Name} WhenTrue:{Dump(StateWhenTrue)} WhenFalse:{Dump(StateWhenFalse)}{"}"}";
}
else
{
return $"{{{GetType().Name} {Dump(State)}{"}"}";
}
}
// For purpose of nullability analysis, awaits create pending branches, so async usings and foreachs do too
public sealed override bool AwaitUsingAndForeachAddsPendingBranch => true;
protected override bool ConvertInsufficientExecutionStackExceptionToCancelledByStackGuardException()
{
return true;
}
protected override ImmutableArray<PendingBranch> Scan(ref bool badRegion)
{
if (_returnTypesOpt != null)
{
_returnTypesOpt.Clear();
}
this.Diagnostics.Clear();
ParameterSymbol methodThisParameter = MethodThisParameter;
this.regionPlace = RegionPlace.Before;
EnterParameters(); // assign parameters
if (!(methodThisParameter is null))
{
EnterParameter(methodThisParameter, methodThisParameter.TypeWithAnnotations);
}
ImmutableArray<PendingBranch> pendingReturns = base.Scan(ref badRegion);
return pendingReturns;
}
internal static void Analyze(
CSharpCompilation compilation,
MethodSymbol method,
BoundNode node,
DiagnosticBag diagnostics)
{
if (method.IsImplicitlyDeclared && !method.IsImplicitConstructor && !method.IsScriptInitializer)
{
return;
}
var binder = compilation.GetBinderFactory(node.SyntaxTree).GetBinder(node.Syntax);
var conversions = binder.Conversions;
Analyze(compilation,
method,
node,
binder,
conversions,
diagnostics,
useMethodSignatureParameterTypes: false,
methodSignatureOpt: method,
returnTypes: null,
initialState: null,
analyzedNullabilityMapOpt: null);
}
internal static BoundNode AnalyzeAndRewrite(
CSharpCompilation compilation,
Symbol symbol,
BoundNode node,
Binder binder,
DiagnosticBag diagnostics)
{
var analyzedNullabilities = PooledDictionary<BoundExpression, (NullabilityInfo, TypeSymbol)>.GetInstance();
var methodSymbol = symbol as MethodSymbol;
Analyze(
compilation,
symbol,
node,
binder,
binder.Conversions,
diagnostics,
useMethodSignatureParameterTypes: !(methodSymbol is null),
methodSignatureOpt: methodSymbol,
returnTypes: null,
initialState: null,
analyzedNullabilityMapOpt: analyzedNullabilities);
var analyzedNullabilitiesMap = analyzedNullabilities.ToImmutableDictionaryAndFree();
#if DEBUG
// https://github.com/dotnet/roslyn/issues/34993 Enable for all calls
if (compilation.NullableAnalysisEnabled)
{
DebugVerifier.Verify(analyzedNullabilitiesMap, node);
}
#endif
return new NullabilityRewriter(analyzedNullabilitiesMap).Visit(node);
}
internal static void AnalyzeIfNeeded(
Binder binder,
BoundAttribute attribute,
DiagnosticBag diagnostics)
{
var compilation = binder.Compilation;
if (compilation.LanguageVersion < MessageID.IDS_FeatureNullableReferenceTypes.RequiredVersion())
{
return;
}
Analyze(
compilation,
symbol: null,
attribute,
binder,
binder.Conversions,
diagnostics,
useMethodSignatureParameterTypes: false,
methodSignatureOpt: null,
returnTypes: null,
initialState: null,
analyzedNullabilityMapOpt: null);
}
internal static void Analyze(
CSharpCompilation compilation,
BoundLambda lambda,
Conversions conversions,
DiagnosticBag diagnostics,
MethodSymbol delegateInvokeMethod,
ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)> returnTypes,
VariableState initialState,
Dictionary<BoundExpression, (NullabilityInfo, TypeSymbol)> analyzedNullabilityMapOpt)
{
Analyze(
compilation,
lambda.Symbol,
lambda.Body,
lambda.Binder,
conversions,
diagnostics,
useMethodSignatureParameterTypes: !lambda.UnboundLambda.HasExplicitlyTypedParameterList,
methodSignatureOpt: delegateInvokeMethod,
returnTypes,
initialState,
analyzedNullabilityMapOpt);
}
private static void Analyze(
CSharpCompilation compilation,
Symbol symbol,
BoundNode node,
Binder binder,
Conversions conversions,
DiagnosticBag diagnostics,
bool useMethodSignatureParameterTypes,
MethodSymbol methodSignatureOpt,
ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)> returnTypes,
VariableState initialState,
Dictionary<BoundExpression, (NullabilityInfo, TypeSymbol)> analyzedNullabilityMapOpt)
{
Debug.Assert(diagnostics != null);
var walker = new NullableWalker(
compilation,
symbol,
useMethodSignatureParameterTypes,
methodSignatureOpt,
node,
binder,
conversions,
returnTypes,
initialState,
analyzedNullabilityMapOpt);
try
{
bool badRegion = false;
Optional<LocalState> initialLocalState = initialState is null ? default : new Optional<LocalState>(initialState.VariableNullableStates);
ImmutableArray<PendingBranch> returns = walker.Analyze(ref badRegion, initialLocalState);
diagnostics.AddRange(walker.Diagnostics);
Debug.Assert(!badRegion);
}
catch (CancelledByStackGuardException ex) when (diagnostics != null)
{
ex.AddAnError(diagnostics);
}
finally
{
walker.Free();
}
}
protected override void Normalize(ref LocalState state)
{
if (!state.Reachable)
return;
int oldNext = state.Capacity;
state.EnsureCapacity(nextVariableSlot);
Populate(ref state, oldNext);
}
private void Populate(ref LocalState state, int start)
{
int capacity = state.Capacity;
for (int slot = start; slot < capacity; slot++)
{
state[slot] = GetDefaultState(ref state, slot);
}
}
private NullableFlowState GetDefaultState(ref LocalState state, int slot)
{
if (!state.Reachable)
return NullableFlowState.NotNull;
if (slot == 0)
return NullableFlowState.MaybeNull;
var variable = variableBySlot[slot];
var symbol = variable.Symbol;
switch (symbol.Kind)
{
case SymbolKind.Local:
// Locals are considered not null before they are definitely assigned
return NullableFlowState.NotNull;
case SymbolKind.Parameter:
{
var parameter = (ParameterSymbol)symbol;
if (parameter.RefKind == RefKind.Out)
{
return NullableFlowState.NotNull;
}
if (!_variableTypes.TryGetValue(parameter, out TypeWithAnnotations parameterType))
{
parameterType = parameter.TypeWithAnnotations;
}
return parameterType.ToTypeWithState().State;
}
case SymbolKind.Field:
case SymbolKind.Property:
case SymbolKind.Event:
return symbol.GetTypeOrReturnType().ToTypeWithState().State;
case SymbolKind.ErrorType:
return NullableFlowState.NotNull;
default:
throw ExceptionUtilities.UnexpectedValue(symbol.Kind);
}
}
protected override bool TryGetReceiverAndMember(BoundExpression expr, out BoundExpression receiver, out Symbol member)
{
receiver = null;
member = null;
switch (expr.Kind)
{
case BoundKind.FieldAccess:
{
var fieldAccess = (BoundFieldAccess)expr;
var fieldSymbol = fieldAccess.FieldSymbol;
member = fieldSymbol;
if (fieldSymbol.IsFixedSizeBuffer)
{
return false;
}
if (fieldSymbol.IsStatic)
{
return true;
}
receiver = fieldAccess.ReceiverOpt;
break;
}
case BoundKind.EventAccess:
{
var eventAccess = (BoundEventAccess)expr;
var eventSymbol = eventAccess.EventSymbol;
// https://github.com/dotnet/roslyn/issues/29901 Use AssociatedField for field-like events?
member = eventSymbol;
if (eventSymbol.IsStatic)
{
return true;
}
receiver = eventAccess.ReceiverOpt;
break;
}
case BoundKind.PropertyAccess:
{
var propAccess = (BoundPropertyAccess)expr;
var propSymbol = propAccess.PropertySymbol;
member = propSymbol;
if (propSymbol.IsStatic)
{
return true;
}
receiver = propAccess.ReceiverOpt;
break;
}
}
Debug.Assert(member?.IsStatic != true);
return (object)member != null &&
(object)receiver != null &&
receiver.Kind != BoundKind.TypeExpression &&
(object)receiver.Type != null;
}
protected override int MakeSlot(BoundExpression node)
{
switch (node.Kind)
{
case BoundKind.ThisReference:
case BoundKind.BaseReference:
{
var method = getTopLevelMethod(_symbol as MethodSymbol);
var thisParameter = method?.ThisParameter;
return (object)thisParameter != null ? GetOrCreateSlot(thisParameter) : -1;
}
case BoundKind.Conversion:
{
int slot = getPlaceholderSlot(node);
if (slot > 0)
{
return slot;
}
var conv = (BoundConversion)node;
switch (conv.Conversion.Kind)
{
case ConversionKind.ExplicitNullable:
{
var operand = conv.Operand;
var operandType = operand.Type;
var convertedType = conv.Type;
if (AreNullableAndUnderlyingTypes(operandType, convertedType, out _))
{
// Explicit conversion of Nullable<T> to T is equivalent to Nullable<T>.Value.
// For instance, in the following, when evaluating `((A)a).B` we need to recognize
// the nullability of `(A)a` (not nullable) and the slot (the slot for `a.Value`).
// struct A { B? B; }
// struct B { }
// if (a?.B != null) _ = ((A)a).B.Value; // no warning
int containingSlot = MakeSlot(operand);
return containingSlot < 0 ? -1 : GetNullableOfTValueSlot(operandType, containingSlot, out _);
}
}
break;
case ConversionKind.Identity:
case ConversionKind.ImplicitReference:
case ConversionKind.ExplicitReference:
case ConversionKind.ImplicitTupleLiteral:
case ConversionKind.ExplicitTupleLiteral:
case ConversionKind.Boxing:
case ConversionKind.Unboxing:
// No need to create a slot for the boxed value (in the Boxing case) since assignment already
// clones slots and there is not another scenario where creating a slot is observable.
return MakeSlot(conv.Operand);
}
}
break;
case BoundKind.DefaultExpression:
case BoundKind.ObjectCreationExpression:
case BoundKind.DynamicObjectCreationExpression:
case BoundKind.AnonymousObjectCreationExpression:
case BoundKind.NewT:
case BoundKind.TupleLiteral:
case BoundKind.ConvertedTupleLiteral:
return getPlaceholderSlot(node);
case BoundKind.ConditionalReceiver:
{
return _lastConditionalAccessSlot;
}
default:
{
int slot = getPlaceholderSlot(node);
return (slot > 0) ? slot : base.MakeSlot(node);
}
}
return -1;
int getPlaceholderSlot(BoundExpression expr)
{
if (_placeholderLocalsOpt != null && _placeholderLocalsOpt.TryGetValue(expr, out PlaceholderLocal placeholder))
{
return GetOrCreateSlot(placeholder);
}
return -1;
}
static MethodSymbol getTopLevelMethod(MethodSymbol method)
{
while ((object)method != null)
{
var container = method.ContainingSymbol;
if (container.Kind == SymbolKind.NamedType)
{
return method;
}
method = container as MethodSymbol;
}
return null;
}
}
private void VisitAll<T>(ImmutableArray<T> nodes) where T : BoundNode
{
if (nodes.IsDefault)
{
return;
}
foreach (var node in nodes)
{
Visit(node);
}
}
private void VisitWithoutDiagnostics(BoundNode node)
{
var previousDiagnostics = _disableDiagnostics;
_disableDiagnostics = true;
Visit(node);
_disableDiagnostics = previousDiagnostics;
}
protected override void VisitRvalue(BoundExpression node)
{
Visit(node);
VisitRvalueEpilogue(node);
}
/// <summary>
/// The contents of this method, particularly <see cref="UseRvalueOnly"/>, are problematic when
/// inlined. The methods themselves are small but they end up allocating significantly larger
/// frames due to the use of biggish value types within them. The <see cref="VisitRvalue"/> method
/// is used on a hot path for fluent calls and this size change is enough that it causes us
/// to exceed our thresholds in EndToEndTests.OverflowOnFluentCall.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private void VisitRvalueEpilogue(BoundExpression node)
{
Unsplit();
UseRvalueOnly(node); // drop lvalue part
}
private TypeWithState VisitRvalueWithState(BoundExpression node)
{
VisitRvalue(node);
return ResultType;
}
private TypeWithAnnotations VisitLvalueWithAnnotations(BoundExpression node)
{
Visit(node);
Unsplit();
return LvalueResultType;
}
private static object GetTypeAsDiagnosticArgument(TypeSymbol typeOpt)
{
return typeOpt ?? (object)"<null>";
}
private enum AssignmentKind
{
Assignment,
Return,
Argument,
ForEachIterationVariable
}
/// <summary>
/// Reports top-level nullability problem in assignment.
/// </summary>
private bool ReportNullableAssignmentIfNecessary(
BoundExpression value,
TypeWithAnnotations targetType,
TypeWithState valueType,
bool useLegacyWarnings,
AssignmentKind assignmentKind = AssignmentKind.Assignment,
Symbol target = null,
Conversion conversion = default,
Location location = null)
{
Debug.Assert((object)target != null || assignmentKind != AssignmentKind.Argument);
if (value == null ||
!targetType.HasType ||
targetType.Type.IsValueType ||
targetType.CanBeAssignedNull ||
valueType.IsNotNull)
{
return false;
}
location ??= value.Syntax.GetLocation();
var unwrappedValue = SkipReferenceConversions(value);
if (unwrappedValue.IsSuppressed)
{
return false;
}
HashSet<DiagnosticInfo> useSiteDiagnostics = null;
if (RequiresSafetyWarningWhenNullIntroduced(targetType))
{
if (conversion.Kind == ConversionKind.UnsetConversionKind)
conversion = this._conversions.ClassifyImplicitConversionFromType(valueType.Type, targetType.Type, ref useSiteDiagnostics);
if (conversion.IsImplicit && !conversion.IsDynamic)
{
// For type parameters that cannot be annotated, the analysis must report those
// places where null values first sneak in, like `default`, `null`, and `GetFirstOrDefault`,
// as a safety diagnostic. This is NOT one of those places.
return false;
}
useLegacyWarnings = false;
}
if (reportNullLiteralAssignmentIfNecessary(value, location, valueType.ToTypeWithAnnotations()))
{
return true;
}
if (assignmentKind == AssignmentKind.Argument)
{
ReportDiagnostic(ErrorCode.WRN_NullReferenceArgument, location,
new FormattedSymbol(target, SymbolDisplayFormat.ShortFormat),
new FormattedSymbol(target.ContainingSymbol, SymbolDisplayFormat.MinimallyQualifiedFormat));
}
else if (useLegacyWarnings)
{
ReportNonSafetyDiagnostic(location);
}
else
{
ReportDiagnostic(assignmentKind switch { AssignmentKind.Return => ErrorCode.WRN_NullReferenceReturn, AssignmentKind.ForEachIterationVariable => ErrorCode.WRN_NullReferenceIterationVariable, _ => ErrorCode.WRN_NullReferenceAssignment }, location);
}
return true;
// Report warning converting null literal to non-nullable reference type.
// target (e.g.: `object x = null;` or calling `void F(object y)` with `F(null)`).
bool reportNullLiteralAssignmentIfNecessary(BoundExpression expr, Location location, TypeWithAnnotations exprType)
{
if (expr.ConstantValue?.IsNull != true)
{
return false;
}
// For type parameters that cannot be annotated, the analysis must report those
// places where null values first sneak in, like `default`, `null`, and `GetFirstOrDefault`,
// as a safety diagnostic. This is one of those places.
if (useLegacyWarnings && !RequiresSafetyWarningWhenNullIntroduced(exprType))
{
ReportNonSafetyDiagnostic(location);
}
else
{
ReportDiagnostic(assignmentKind == AssignmentKind.Return ? ErrorCode.WRN_NullReferenceReturn : ErrorCode.WRN_NullAsNonNullable, location);
}
return true;
}
}
private static bool IsDefaultValue(BoundExpression expr)
{
switch (expr.Kind)
{
case BoundKind.Conversion:
{
var conversion = (BoundConversion)expr;
return conversion.Conversion.Kind == ConversionKind.DefaultOrNullLiteral &&
IsDefaultValue(conversion.Operand);
}
case BoundKind.DefaultExpression:
return true;
default:
return false;
}
}
private void ReportNullabilityMismatchInAssignment(SyntaxNode syntaxNode, object sourceType, object destinationType)
{
ReportDiagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, syntaxNode, sourceType, destinationType);
}
private void ReportNullabilityMismatchInAssignment(Location location, object sourceType, object destinationType)
{
ReportDiagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, location, sourceType, destinationType);
}
/// <summary>
/// Update tracked value on assignment.
/// </summary>
private void TrackNullableStateForAssignment(
BoundExpression valueOpt,
TypeWithAnnotations targetType,
int targetSlot,
TypeWithState valueType,
int valueSlot = -1)
{
Debug.Assert(!IsConditionalState);
if (this.State.Reachable)
{
if (!targetType.HasType)
{
return;
}
if (targetSlot <= 0 || targetSlot == valueSlot)
{
return;
}
if (targetSlot >= this.State.Capacity) Normalize(ref this.State);
var newState = valueType.State;
SetStateAndTrackForFinally(ref this.State, targetSlot, newState);
InheritDefaultState(targetSlot);
// https://github.com/dotnet/roslyn/issues/33428: Can the areEquivalentTypes check be removed
// if InheritNullableStateOfMember asserts the member is valid for target and value?
if (areEquivalentTypes(targetType, valueType))
{
if (targetType.Type.IsReferenceType ||
targetType.TypeKind == TypeKind.TypeParameter ||
targetType.IsNullableType())
{
if (valueSlot > 0)
{
InheritNullableStateOfTrackableType(targetSlot, valueSlot, skipSlot: targetSlot);
}
}
else if (EmptyStructTypeCache.IsTrackableStructType(targetType.Type))
{
InheritNullableStateOfTrackableStruct(targetType.Type, targetSlot, valueSlot, isDefaultValue: !(valueOpt is null) && IsDefaultValue(valueOpt), skipSlot: targetSlot);
}
}
}
static bool areEquivalentTypes(TypeWithAnnotations target, TypeWithState assignedValue) =>
target.Type.Equals(assignedValue.Type, TypeCompareKind.AllIgnoreOptions);
}
private void ReportNonSafetyDiagnostic(Location location)
{
ReportDiagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, location);
}
private void ReportDiagnostic(ErrorCode errorCode, SyntaxNode syntaxNode, params object[] arguments)
{
ReportDiagnostic(errorCode, syntaxNode.GetLocation(), arguments);
}
private void ReportDiagnostic(ErrorCode errorCode, Location location, params object[] arguments)
{
Debug.Assert(ErrorFacts.NullableFlowAnalysisWarnings.Contains(MessageProvider.Instance.GetIdForErrorCode((int)errorCode)));
Debug.Assert(!IsConditionalState);
if (this.State.Reachable && !_disableDiagnostics)
{
Diagnostics.Add(errorCode, location, arguments);
}
}
private void InheritNullableStateOfTrackableStruct(TypeSymbol targetType, int targetSlot, int valueSlot, bool isDefaultValue, int skipSlot = -1)
{
Debug.Assert(targetSlot > 0);
Debug.Assert(EmptyStructTypeCache.IsTrackableStructType(targetType));
if (skipSlot < 0)
{
skipSlot = targetSlot;
}
if (!isDefaultValue && valueSlot > 0)
{
InheritNullableStateOfTrackableType(targetSlot, valueSlot, skipSlot);
}
else
{
foreach (var field in _emptyStructTypeCache.GetStructInstanceFields(targetType))
{
InheritNullableStateOfMember(targetSlot, valueSlot, field, isDefaultValue: isDefaultValue, skipSlot);
}
}
}
// 'skipSlot' is the original target slot that should be skipped in case of cycles.
private void InheritNullableStateOfMember(int targetContainerSlot, int valueContainerSlot, Symbol member, bool isDefaultValue, int skipSlot)
{
Debug.Assert(targetContainerSlot > 0);
Debug.Assert(skipSlot > 0);
// https://github.com/dotnet/roslyn/issues/33428: Ensure member is valid for target and value.
TypeWithAnnotations fieldOrPropertyType = member.GetTypeOrReturnType();
if (fieldOrPropertyType.Type.IsReferenceType ||
fieldOrPropertyType.TypeKind == TypeKind.TypeParameter ||
fieldOrPropertyType.IsNullableType())
{
int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot);
if (targetMemberSlot > 0)
{
NullableFlowState value = isDefaultValue ? NullableFlowState.MaybeNull : fieldOrPropertyType.ToTypeWithState().State;
int valueMemberSlot = -1;
if (valueContainerSlot > 0)
{
valueMemberSlot = VariableSlot(member, valueContainerSlot);
if (valueMemberSlot == skipSlot)
{
return;
}
value = valueMemberSlot > 0 && valueMemberSlot < this.State.Capacity ?
this.State[valueMemberSlot] :
NullableFlowState.NotNull;
}
SetStateAndTrackForFinally(ref this.State, targetMemberSlot, value);
if (valueMemberSlot > 0)
{
InheritNullableStateOfTrackableType(targetMemberSlot, valueMemberSlot, skipSlot);
}
}
}
else if (EmptyStructTypeCache.IsTrackableStructType(fieldOrPropertyType.Type))
{
int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot);
if (targetMemberSlot > 0)
{
int valueMemberSlot = (valueContainerSlot > 0) ? GetOrCreateSlot(member, valueContainerSlot) : -1;
if (valueMemberSlot == skipSlot)
{
return;
}
InheritNullableStateOfTrackableStruct(fieldOrPropertyType.Type, targetMemberSlot, valueMemberSlot, isDefaultValue: isDefaultValue, skipSlot);
}
}
}
/// <summary>
/// Whenever assigning a variable, and that variable is not declared at the point the state is being set,
/// and the new state might be <see cref="NullableFlowState.MaybeNull"/>, this method should be called to perform the
/// state setting and to ensure the mutation is visible outside the finally block when the mutation occurs in a
/// finally block.
/// </summary>
private void SetStateAndTrackForFinally(ref LocalState state, int slot, NullableFlowState newState)
{
state[slot] = newState;
if (newState == NullableFlowState.MaybeNull && _tryState.HasValue)
{
var tryState = _tryState.Value;
tryState[slot] = NullableFlowState.MaybeNull;
_tryState = tryState;
}
}
private void InheritDefaultState(int targetSlot)
{
Debug.Assert(targetSlot > 0);
// Reset the state of any members of the target.
for (int slot = targetSlot + 1; slot < nextVariableSlot; slot++)
{
var variable = variableBySlot[slot];
if (variable.ContainingSlot != targetSlot)
{
continue;
}
SetStateAndTrackForFinally(ref this.State, slot, variable.Symbol.GetTypeOrReturnType().ToTypeWithState().State);
InheritDefaultState(slot);
}
}
private void InheritNullableStateOfTrackableType(int targetSlot, int valueSlot, int skipSlot)
{
Debug.Assert(targetSlot > 0);
Debug.Assert(valueSlot > 0);
// Clone the state for members that have been set on the value.
for (int slot = valueSlot + 1; slot < nextVariableSlot; slot++)
{
var variable = variableBySlot[slot];
if (variable.ContainingSlot != valueSlot)
{
continue;
}
var member = variable.Symbol;
Debug.Assert(member.Kind == SymbolKind.Field || member.Kind == SymbolKind.Property || member.Kind == SymbolKind.Event);
InheritNullableStateOfMember(targetSlot, valueSlot, member, isDefaultValue: false, skipSlot);
}
}
private TypeSymbol GetSlotType(int slot)
{
return variableBySlot[slot].Symbol.GetTypeOrReturnType().Type;
}
protected override LocalState TopState()
{
var state = LocalState.ReachableState(capacity: nextVariableSlot);
Populate(ref state, start: 0);
return state;
}
protected override LocalState UnreachableState()
{
return LocalState.UnreachableState;
}
protected override LocalState ReachableBottomState()
{
// Create a reachable state in which all variables are known to be non-null.
return LocalState.ReachableState(capacity: nextVariableSlot);
}
private void EnterParameters()
{
var methodSymbol = _symbol as MethodSymbol;
if (methodSymbol is null)
{
return;
}
var methodParameters = methodSymbol.Parameters;
var signatureParameters = _useMethodSignatureParameterTypes ? _methodSignatureOpt.Parameters : methodParameters;
for (int i = 0; i < methodParameters.Length; i++)
{
var parameter = methodParameters[i];
// In error scenarios, the method can potentially have more parameters than the signature. If so, use the parameter type for those
// errored parameters
var parameterType = i >= signatureParameters.Length ? parameter.TypeWithAnnotations : signatureParameters[i].TypeWithAnnotations;
EnterParameter(parameter, parameterType);
}
}
private void EnterParameter(ParameterSymbol parameter, TypeWithAnnotations parameterType)
{
_variableTypes[parameter] = parameterType;
int slot = GetOrCreateSlot(parameter);
Debug.Assert(!IsConditionalState);
if (slot > 0)
{
if (parameter.RefKind == RefKind.Out)
{
this.State[slot] = NullableFlowState.NotNull;
}
else
{
this.State[slot] = parameterType.ToTypeWithState().State;
if (EmptyStructTypeCache.IsTrackableStructType(parameterType.Type))
{
InheritNullableStateOfTrackableStruct(
parameterType.Type,
slot,
valueSlot: -1,
isDefaultValue: parameter.ExplicitDefaultConstantValue?.IsNull == true);
}
}
}
}
protected override BoundNode VisitReturnStatementNoAdjust(BoundReturnStatement node)
{
Debug.Assert(!IsConditionalState);
BoundExpression expr = node.ExpressionOpt;
if (expr == null)
{
return null;
}
// Should not convert to method return type when inferring return type (when _returnTypesOpt != null).
if (_returnTypesOpt == null &&
TryGetReturnType(out TypeWithAnnotations returnType))
{
if (node.RefKind == RefKind.None)
{
VisitOptionalImplicitConversion(expr, returnType, useLegacyWarnings: false, trackMembers: false, AssignmentKind.Return);
}
else
{
// return ref expr;
VisitRefExpression(expr, returnType);
}
}
else
{
var result = VisitRvalueWithState(expr);
if (_returnTypesOpt != null)
{
_returnTypesOpt.Add((node, result.ToTypeWithAnnotations()));
}
}
return null;
}
private TypeWithState VisitRefExpression(BoundExpression expr, TypeWithAnnotations destinationType)
{
Visit(expr);
TypeWithState resultType = ResultType;
if (!expr.IsSuppressed && RemoveConversion(expr, includeExplicitConversions: false).expression.Kind != BoundKind.ThrowExpression)
{
var lvalueResultType = LvalueResultType;
if (IsNullabilityMismatch(lvalueResultType, destinationType))
{
// declared types must match
ReportNullabilityMismatchInAssignment(expr.Syntax, lvalueResultType, destinationType);
}
else
{
// types match, but state would let a null in
ReportNullableAssignmentIfNecessary(expr, destinationType, resultType, useLegacyWarnings: false);
}
}
return resultType;
}
private bool TryGetReturnType(out TypeWithAnnotations type)
{
var method = _symbol as MethodSymbol;
if (method is null)
{
type = default;
return false;
}
var returnType = (_methodSignatureOpt ?? method).ReturnTypeWithAnnotations;
Debug.Assert((object)returnType != LambdaSymbol.ReturnTypeIsBeingInferred);
if (returnType.IsVoidType())
{
type = default;
return false;
}
if (!method.IsAsync)
{
type = returnType;
return true;
}
if (method.IsGenericTaskReturningAsync(compilation))
{
type = ((NamedTypeSymbol)returnType.Type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics.Single();
return true;
}
type = default;
return false;
}
private static bool RequiresSafetyWarningWhenNullIntroduced(TypeWithAnnotations typeWithAnnotations)
{
return
typeWithAnnotations is { Type: TypeSymbol type, NullableAnnotation: NullableAnnotation.NotAnnotated } &&
type.IsTypeParameterDisallowingAnnotation() &&
!type.IsNullableTypeOrTypeParameter();
}
private static bool RequiresSafetyWarningWhenNullIntroduced(TypeSymbol type)
{
return
type.IsTypeParameterDisallowingAnnotation() &&
!type.IsNullableTypeOrTypeParameter();
}
public override BoundNode VisitLocal(BoundLocal node)
{
var local = node.LocalSymbol;
int slot = GetOrCreateSlot(local);
var type = GetDeclaredLocalResult(local);
if (!node.Type.Equals(type.Type, TypeCompareKind.ConsiderEverything | TypeCompareKind.IgnoreNullableModifiersForReferenceTypes | TypeCompareKind.IgnoreDynamicAndTupleNames))
{
// When the local is used before or during initialization, there can potentially be a mismatch between node.LocalSymbol.Type and node.Type. We
// need to prefer node.Type as we shouldn't be changing the type of the BoundLocal node during rewrite.
// https://github.com/dotnet/roslyn/issues/34158
Debug.Assert(node.Type.IsErrorType() || type.Type.IsErrorType());
type = TypeWithAnnotations.Create(node.Type, type.NullableAnnotation);
}
SetResult(node, GetAdjustedResult(type, slot), type);
return null;
}
public override BoundNode VisitLocalDeclaration(BoundLocalDeclaration node)
{
var local = node.LocalSymbol;
int slot = GetOrCreateSlot(local);
// We need visit the optional arguments so that we can return nullability information
// about them, but we don't want to communciate any information about anything underneath.
// Additionally, tests like Scope_DeclaratorArguments_06 can have conditional expressions
// in the optional arguments that can leave us in a split state, so we want to make sure
// we are not in a conditional state after.
Debug.Assert(!IsConditionalState);
var oldDisable = _disableDiagnostics;
_disableDiagnostics = true;
var currentState = State;
VisitAll(node.ArgumentsOpt);
_disableDiagnostics = oldDisable;
SetState(currentState);
if (node.DeclaredTypeOpt != null)
{
VisitTypeExpression(node.DeclaredTypeOpt);
}
var initializer = node.InitializerOpt;
if (initializer is null)
{
return null;
}
TypeWithAnnotations type = local.TypeWithAnnotations;
TypeWithState valueType;
if (local.IsRef)
{
valueType = VisitRefExpression(initializer, type);
}
else
{
bool inferredType = node.InferredType;
valueType = VisitOptionalImplicitConversion(initializer, targetTypeOpt: inferredType ? default : type, useLegacyWarnings: true, trackMembers: true, AssignmentKind.Assignment);
if (inferredType)
{
if (valueType.HasNullType)
{
Debug.Assert(type.Type.IsErrorType());
valueType = type.ToTypeWithState();
}
type = valueType.ToTypeWithAnnotations();
_variableTypes[local] = type;
}
}
TrackNullableStateForAssignment(initializer, type, slot, valueType, MakeSlot(initializer));
return null;
}
protected override BoundExpression VisitExpressionWithoutStackGuard(BoundExpression node)
{
Debug.Assert(!IsConditionalState);
SetInvalidResult();
_ = base.VisitExpressionWithoutStackGuard(node);
TypeWithState resultType = ResultType;
#if DEBUG
// Verify Visit method set _result.
Debug.Assert((object)resultType.Type != _invalidType.Type);
Debug.Assert(AreCloseEnough(resultType.Type, node.Type));
#endif
if (ShouldMakeNotNullRvalue(node))
{
var result = resultType.WithNotNullState();
SetResult(node, result, LvalueResultType);
}
return null;
}
#if DEBUG
// For asserts only.
private static bool AreCloseEnough(TypeSymbol typeA, TypeSymbol typeB)
{
// https://github.com/dotnet/roslyn/issues/34993: We should be able to tighten this to ensure that we're actually always returning the same type,
// not error if one is null or ignoring certain types
if ((object)typeA == typeB)
{
return true;
}
if (typeA is null || typeB is null)
{
return typeA?.IsErrorType() != false && typeB?.IsErrorType() != false;
}
return canIgnoreAnyType(typeA) ||
canIgnoreAnyType(typeB) ||
typeA.Equals(typeB, TypeCompareKind.IgnoreCustomModifiersAndArraySizesAndLowerBounds | TypeCompareKind.IgnoreNullableModifiersForReferenceTypes | TypeCompareKind.IgnoreDynamicAndTupleNames); // Ignore TupleElementNames (see https://github.com/dotnet/roslyn/issues/23651).
bool canIgnoreAnyType(TypeSymbol type)
{
return (object)type.VisitType((t, unused1, unused2) => canIgnoreType(t), (object)null) != null;
}
bool canIgnoreType(TypeSymbol type)
{
return type.IsErrorType() || type.IsDynamic() || type.HasUseSiteError || (type.IsAnonymousType && canIgnoreAnonymousType((NamedTypeSymbol)type));
}
bool canIgnoreAnonymousType(NamedTypeSymbol type)
{
return AnonymousTypeManager.GetAnonymousTypePropertyTypesWithAnnotations(type).Any(t => canIgnoreAnyType(t.Type));
}
}
#endif
protected override void VisitStatement(BoundStatement statement)
{
SetInvalidResult();
base.VisitStatement(statement);
SetInvalidResult();
}
public override BoundNode VisitObjectCreationExpression(BoundObjectCreationExpression node)
{
Debug.Assert(!IsConditionalState);
var arguments = node.Arguments;
var argumentResults = VisitArguments(node, arguments, node.ArgumentRefKindsOpt, node.Constructor, node.ArgsToParamsOpt, node.Expanded);
VisitObjectOrDynamicObjectCreation(node, arguments, argumentResults, node.InitializerExpressionOpt);
return null;
}
private void VisitObjectOrDynamicObjectCreation(
BoundExpression node,
ImmutableArray<BoundExpression> arguments,
ImmutableArray<VisitArgumentResult> argumentResults,
BoundExpression initializerOpt)
{
Debug.Assert(node.Kind == BoundKind.ObjectCreationExpression ||
node.Kind == BoundKind.DynamicObjectCreationExpression ||
node.Kind == BoundKind.NewT);
var argumentTypes = argumentResults.SelectAsArray(ar => ar.RValueType);
int slot = -1;
TypeSymbol type = node.Type;
NullableFlowState resultState = NullableFlowState.NotNull;
if ((object)type != null)
{
slot = GetOrCreatePlaceholderSlot(node);
if (slot > 0)
{
var constructor = (node as BoundObjectCreationExpression)?.Constructor;
bool isDefaultValueTypeConstructor = constructor?.IsDefaultValueTypeConstructor() == true;
if (EmptyStructTypeCache.IsTrackableStructType(type))
{
var tupleType = constructor?.ContainingType as TupleTypeSymbol;
if ((object)tupleType != null && !isDefaultValueTypeConstructor)
{
// new System.ValueTuple<T1, ..., TN>(e1, ..., eN)
TrackNullableStateOfTupleElements(slot, tupleType, arguments, argumentTypes, useRestField: true);
}
else
{
InheritNullableStateOfTrackableStruct(
type,
slot,
valueSlot: -1,
isDefaultValue: isDefaultValueTypeConstructor);
}
}
else if (type.IsNullableType())
{
if (isDefaultValueTypeConstructor)
{
// a nullable value type created with its default constructor is by definition null
resultState = NullableFlowState.MaybeNull;
}
else if (constructor.ParameterCount == 1)
{
// if we deal with one-parameter ctor that takes underlying, then Value state is inferred from the argument.
var parameterType = constructor.ParameterTypesWithAnnotations[0];
if (AreNullableAndUnderlyingTypes(type, parameterType.Type, out TypeWithAnnotations underlyingType))
{
var operand = arguments[0];
int valueSlot = MakeSlot(operand);
if (valueSlot > 0)
{
TrackNullableStateOfNullableValue(slot, type, operand, underlyingType.ToTypeWithState(), valueSlot);
}
}
}
}
this.State[slot] = resultState;
}
}
if (initializerOpt != null)
{
VisitObjectCreationInitializer(null, slot, initializerOpt);
}
SetResultType(node, TypeWithState.Create(type, resultState));
}
private void VisitObjectCreationInitializer(Symbol containingSymbol, int containingSlot, BoundExpression node)
{
switch (node.Kind)
{
case BoundKind.ObjectInitializerExpression:
checkImplicitReceiver();
foreach (var initializer in ((BoundObjectInitializerExpression)node).Initializers)
{
switch (initializer.Kind)
{
case BoundKind.AssignmentOperator:
VisitObjectElementInitializer(containingSlot, (BoundAssignmentOperator)initializer);
break;
default:
VisitRvalue(initializer);
break;
}
}
break;
case BoundKind.CollectionInitializerExpression:
checkImplicitReceiver();
foreach (var initializer in ((BoundCollectionInitializerExpression)node).Initializers)
{
switch (initializer.Kind)
{
case BoundKind.CollectionElementInitializer:
VisitCollectionElementInitializer((BoundCollectionElementInitializer)initializer);
break;
default:
VisitRvalue(initializer);
break;
}
}
break;
default:
Debug.Assert((object)containingSymbol != null);
if ((object)containingSymbol != null)
{
var type = containingSymbol.GetTypeOrReturnType();
TypeWithState resultType = VisitOptionalImplicitConversion(node, type, useLegacyWarnings: false, trackMembers: true, AssignmentKind.Assignment);
TrackNullableStateForAssignment(node, type, containingSlot, resultType, MakeSlot(node));
}
break;
}
void checkImplicitReceiver()
{
if (containingSlot >= 0)
{
_ = ReportPossibleNullReceiverIfNeeded(node.Type, this.State[containingSlot], checkNullableValueType: false, node.Syntax, out _);
}
}
}
private void VisitObjectElementInitializer(int containingSlot, BoundAssignmentOperator node)
{
var left = node.Left;
switch (left.Kind)
{
case BoundKind.ObjectInitializerMember:
{
var objectInitializer = (BoundObjectInitializerMember)left;
var symbol = objectInitializer.MemberSymbol;
if (!objectInitializer.Arguments.IsDefaultOrEmpty)
{
VisitArguments(objectInitializer, objectInitializer.Arguments, objectInitializer.ArgumentRefKindsOpt, (PropertySymbol)symbol, objectInitializer.ArgsToParamsOpt, objectInitializer.Expanded);
}
if ((object)symbol != null)
{
int slot = (containingSlot < 0) ? -1 : GetOrCreateSlot(symbol, containingSlot);
VisitObjectCreationInitializer(symbol, slot, node.Right);
// https://github.com/dotnet/roslyn/issues/35040: Should likely be setting _resultType in VisitObjectCreationInitializer
// and using that value instead of reconstructing here
}
var result = new VisitResult(objectInitializer.Type, NullableAnnotation.NotAnnotated, NullableFlowState.NotNull);
SetAnalyzedNullability(objectInitializer, result);
SetAnalyzedNullability(node, result);
}
break;
default:
Visit(node);
break;
}
}
private new void VisitCollectionElementInitializer(BoundCollectionElementInitializer node)
{
// Note: we analyze even omitted calls
VisitArguments(node, node.Arguments, refKindsOpt: default, node.AddMethod, node.ArgsToParamsOpt, node.Expanded);
if (node.ImplicitReceiverOpt != null)
{
Debug.Assert(node.ImplicitReceiverOpt.Kind == BoundKind.ImplicitReceiver);
SetAnalyzedNullability(node.ImplicitReceiverOpt, new VisitResult(node.ImplicitReceiverOpt.Type, NullableAnnotation.NotAnnotated, NullableFlowState.NotNull));
}
SetUnknownResultNullability(node);
}
private void SetNotNullResult(BoundExpression node)
{
SetResultType(node, TypeWithState.Create(node.Type, NullableFlowState.NotNull));
}
/// <summary>
/// Returns true if the type is a struct with no fields or properties.
/// </summary>
protected override bool IsEmptyStructType(TypeSymbol type)
{
if (type.TypeKind != TypeKind.Struct)
{
return false;
}
// EmptyStructTypeCache.IsEmptyStructType() returns false
// if there are non-cyclic fields.
if (!_emptyStructTypeCache.IsEmptyStructType(type))
{
return false;
}
if (type.SpecialType != SpecialType.None)
{
return true;
}
var members = ((NamedTypeSymbol)type).GetMembersUnordered();
// EmptyStructTypeCache.IsEmptyStructType() returned true. If there
// are fields, those must be cyclic, so treat the type as empty.
if (members.Any(m => m.Kind == SymbolKind.Field))
{
return true;
}
// If there are properties, the type is not empty.
if (members.Any(m => m.Kind == SymbolKind.Property))
{
return false;
}
return true;
}
private int GetOrCreatePlaceholderSlot(BoundExpression node)
{
if (IsEmptyStructType(node.Type))
{
return -1;
}
return GetOrCreatePlaceholderSlot(node, TypeWithAnnotations.Create(node.Type, NullableAnnotation.NotAnnotated));
}
private int GetOrCreatePlaceholderSlot(object identifier, TypeWithAnnotations type)
{
_placeholderLocalsOpt ??= PooledDictionary<object, PlaceholderLocal>.GetInstance();
if (!_placeholderLocalsOpt.TryGetValue(identifier, out PlaceholderLocal placeholder))
{
placeholder = new PlaceholderLocal(_symbol, identifier, type);
_placeholderLocalsOpt.Add(identifier, placeholder);
}
Debug.Assert((object)placeholder != null);
return GetOrCreateSlot(placeholder, forceSlotEvenIfEmpty: true);
}
public override BoundNode VisitAnonymousObjectCreationExpression(BoundAnonymousObjectCreationExpression node)
{
Debug.Assert(!IsConditionalState);
Debug.Assert(node.Type.IsAnonymousType);
var anonymousType = (NamedTypeSymbol)node.Type;
var arguments = node.Arguments;
var argumentTypes = arguments.SelectAsArray((arg, self) =>
self.VisitRvalueWithState(arg), this);
var argumentsWithAnnotations = argumentTypes.SelectAsArray(arg =>
arg.ToTypeWithAnnotations());
if (argumentsWithAnnotations.All(argType => argType.HasType))
{
anonymousType = AnonymousTypeManager.ConstructAnonymousTypeSymbol(anonymousType, argumentsWithAnnotations);
int receiverSlot = GetOrCreatePlaceholderSlot(node);
int currentDeclarationIndex = 0;
for (int i = 0; i < arguments.Length; i++)
{
var argument = arguments[i];
var argumentType = argumentTypes[i];
var property = AnonymousTypeManager.GetAnonymousTypeProperty(anonymousType, i);
TrackNullableStateForAssignment(argument, property.TypeWithAnnotations, GetOrCreateSlot(property, receiverSlot), argumentType, MakeSlot(argument));
var currentDeclaration = getDeclaration(node, property, ref currentDeclarationIndex);
if (!(currentDeclaration is null))
{
SetAnalyzedNullability(currentDeclaration, new VisitResult(argumentType, property.TypeWithAnnotations));
}
}
}
SetResultType(node, TypeWithState.Create(anonymousType, NullableFlowState.NotNull));
return null;
static BoundAnonymousPropertyDeclaration getDeclaration(BoundAnonymousObjectCreationExpression node, PropertySymbol currentProperty, ref int currentDeclarationIndex)
{
if (currentDeclarationIndex >= node.Declarations.Length)
{
return null;
}
var currentDeclaration = node.Declarations[currentDeclarationIndex];
// https://github.com/dotnet/roslyn/issues/35044: This works for simple success cases, but does not work for failures. Likely will have to do something more complicated here involving rebinding the
// declarators based on the newly constructed anonymous type symbol above and matching them to the existing symbol
if (currentDeclaration.Property.Name == currentProperty.Name &&
currentDeclaration.Property.Type.Equals(currentProperty.Type, TypeCompareKind.ConsiderEverything | TypeCompareKind.AllNullableIgnoreOptions))
{
currentDeclarationIndex++;
return currentDeclaration;
}
return null;
}
}
public override BoundNode VisitArrayCreation(BoundArrayCreation node)
{
foreach (var expr in node.Bounds)
{
VisitRvalue(expr);
}
TypeSymbol resultType = (node.InitializerOpt == null) ? node.Type : VisitArrayInitializer(node);
SetResultType(node, TypeWithState.Create(resultType, NullableFlowState.NotNull));
return null;
}
private ArrayTypeSymbol VisitArrayInitializer(BoundArrayCreation node)
{
BoundArrayInitialization initialization = node.InitializerOpt;
var expressions = ArrayBuilder<BoundExpression>.GetInstance(initialization.Initializers.Length);
GetArrayElements(initialization, expressions);
int n = expressions.Count;
// Consider recording in the BoundArrayCreation
// whether the array was implicitly typed, rather than relying on syntax.
bool isInferred = node.Syntax.Kind() == SyntaxKind.ImplicitArrayCreationExpression;
var arrayType = (ArrayTypeSymbol)node.Type;
var elementType = arrayType.ElementTypeWithAnnotations;
if (!isInferred)
{
foreach (var expr in expressions)
{
_ = VisitOptionalImplicitConversion(expr, elementType, useLegacyWarnings: false, trackMembers: false, AssignmentKind.Assignment);
}
}
else
{
var expressionsNoConversions = ArrayBuilder<BoundExpression>.GetInstance(n);
var conversions = ArrayBuilder<Conversion>.GetInstance(n);
var resultTypes = ArrayBuilder<TypeWithState>.GetInstance(n);
var placeholderBuilder = ArrayBuilder<BoundExpression>.GetInstance(n);
foreach (var expression in expressions)
{
// collect expressions, conversions and result types
(BoundExpression expressionNoConversion, Conversion conversion) = RemoveConversion(expression, includeExplicitConversions: false);
expressionsNoConversions.Add(expressionNoConversion);
conversions.Add(conversion);
var resultType = VisitRvalueWithState(expressionNoConversion);
resultTypes.Add(resultType);
placeholderBuilder.Add(CreatePlaceholderIfNecessary(expressionNoConversion, resultType.ToTypeWithAnnotations()));
}
var placeholders = placeholderBuilder.ToImmutableAndFree();
TypeSymbol bestType = null;
if (!node.HasErrors)
{
HashSet<DiagnosticInfo> useSiteDiagnostics = null;
bestType = BestTypeInferrer.InferBestType(placeholders, _conversions, ref useSiteDiagnostics);
}
TypeWithAnnotations inferredType = (bestType is null)
? elementType.SetUnknownNullabilityForReferenceTypes()
: TypeWithAnnotations.Create(bestType);
if ((object)bestType != null)
{
// Convert elements to best type to determine element top-level nullability and to report nested nullability warnings
for (int i = 0; i < n; i++)
{
var expressionNoConversion = expressionsNoConversions[i];
var expression = GetConversionIfApplicable(expressions[i], expressionNoConversion);
resultTypes[i] = VisitConversion(expression, expressionNoConversion, conversions[i], inferredType, resultTypes[i], checkConversion: true,
fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: true, reportTopLevelWarnings: false);
}
// Set top-level nullability on inferred element type
var elementState = BestTypeInferrer.GetNullableState(resultTypes);
inferredType = TypeWithState.Create(inferredType.Type, elementState).ToTypeWithAnnotations();
for (int i = 0; i < n; i++)
{
// Report top-level warnings
_ = VisitConversion(conversionOpt: null, conversionOperand: expressionsNoConversions[i], Conversion.Identity, targetTypeWithNullability: inferredType, operandType: resultTypes[i],
checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: false);
}
}
expressionsNoConversions.Free();
conversions.Free();
resultTypes.Free();
arrayType = arrayType.WithElementType(inferredType);
}
expressions.Free();
SetInvalidResult();
return arrayType;
}
/// <summary>
/// Applies a method similar to <see cref="VisitArrayInitializer(BoundArrayCreation)"/>
/// The expressions returned from a lambda are not converted though, so we'll have to classify fresh conversions.
/// Note: even if some conversions fail, we'll proceed to infer top-level nullability. That is reasonable in common cases.
/// </summary>
internal static TypeWithAnnotations BestTypeForLambdaReturns(
ArrayBuilder<(BoundExpression, TypeWithAnnotations)> returns,
CSharpCompilation compilation,
BoundNode node,
Conversions conversions)
{
var walker = new NullableWalker(compilation,
symbol: null,
useMethodSignatureParameterTypes: false,
methodSignatureOpt: null,
node,
binder: null,
conversions: conversions,
returnTypesOpt: null,
initialState: null,
analyzedNullabilityMapOpt: null);
int n = returns.Count;
var resultTypes = ArrayBuilder<TypeWithAnnotations>.GetInstance(n);
var placeholdersBuilder = ArrayBuilder<BoundExpression>.GetInstance(n);
for (int i = 0; i < n; i++)
{
var (returnExpr, resultType) = returns[i];
resultTypes.Add(resultType);
placeholdersBuilder.Add(CreatePlaceholderIfNecessary(returnExpr, resultType));
}
HashSet<DiagnosticInfo> useSiteDiagnostics = null;
var placeholders = placeholdersBuilder.ToImmutableAndFree();
TypeSymbol bestType = BestTypeInferrer.InferBestType(placeholders, walker._conversions, ref useSiteDiagnostics);
TypeWithAnnotations inferredType;
if ((object)bestType != null)
{
// Note: so long as we have a best type, we can proceed.
var bestTypeWithObliviousAnnotation = TypeWithAnnotations.Create(bestType);
ConversionsBase conversionsWithoutNullability = walker._conversions.WithNullability(false);
for (int i = 0; i < n; i++)
{
BoundExpression placeholder = placeholders[i];
Conversion conversion = conversionsWithoutNullability.ClassifyConversionFromExpression(placeholder, bestType, ref useSiteDiagnostics);
resultTypes[i] = walker.VisitConversion(conversionOpt: null, placeholder, conversion, bestTypeWithObliviousAnnotation, resultTypes[i].ToTypeWithState(),
checkConversion: false, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Return,
reportRemainingWarnings: false, reportTopLevelWarnings: false).ToTypeWithAnnotations();
}
// Set top-level nullability on inferred type
inferredType = TypeWithAnnotations.Create(bestType, BestTypeInferrer.GetNullableAnnotation(resultTypes));
}
else
{
inferredType = default;
}
resultTypes.Free();
walker.Free();
return inferredType;
}
private static void GetArrayElements(BoundArrayInitialization node, ArrayBuilder<BoundExpression> builder)
{
foreach (var child in node.Initializers)
{
if (child.Kind == BoundKind.ArrayInitialization)
{
GetArrayElements((BoundArrayInitialization)child, builder);
}
else
{
builder.Add(child);
}
}
}
public override BoundNode VisitArrayAccess(BoundArrayAccess node)
{
Debug.Assert(!IsConditionalState);
Visit(node.Expression);
Debug.Assert(!IsConditionalState);
Debug.Assert(!node.Expression.Type.IsValueType);
// https://github.com/dotnet/roslyn/issues/30598: Mark receiver as not null
// after indices have been visited, and only if the receiver has not changed.
_ = CheckPossibleNullReceiver(node.Expression);
var type = ResultType.Type as ArrayTypeSymbol;
foreach (var i in node.Indices)
{
VisitRvalue(i);
}
TypeWithAnnotations result;
if (node.Indices.Length == 1 &&
TypeSymbol.Equals(node.Indices[0].Type, compilation.GetWellKnownType(WellKnownType.System_Range), TypeCompareKind.ConsiderEverything2))
{
result = TypeWithAnnotations.Create(type);
}
else
{
result = type?.ElementTypeWithAnnotations ?? default;
}
SetLvalueResultType(node, result);
return null;
}
private TypeWithState InferResultNullability(BoundBinaryOperator node, TypeWithState leftType, TypeWithState rightType)
{
return InferResultNullability(node.OperatorKind, node.MethodOpt, node.Type, leftType, rightType);
}
private TypeWithState InferResultNullability(BinaryOperatorKind operatorKind, MethodSymbol methodOpt, TypeSymbol resultType, TypeWithState leftType, TypeWithState rightType)
{
NullableFlowState resultState = NullableFlowState.NotNull;
if (operatorKind.IsUserDefined())
{
// Update method based on operand types: see https://github.com/dotnet/roslyn/issues/29605.
if ((object)methodOpt != null && methodOpt.ParameterCount == 2)
{
return operatorKind.IsLifted() && !operatorKind.IsComparison()
? LiftedReturnType(methodOpt.ReturnTypeWithAnnotations, leftType.State.Join(rightType.State))
: methodOpt.ReturnTypeWithAnnotations.ToTypeWithState();
}
}
else if (!operatorKind.IsDynamic() && !resultType.IsValueType)
{
switch (operatorKind.Operator() | operatorKind.OperandTypes())
{
case BinaryOperatorKind.DelegateCombination:
resultState = leftType.State.Meet(rightType.State);
break;
case BinaryOperatorKind.DelegateRemoval:
resultState = NullableFlowState.MaybeNull; // Delegate removal can produce null.
break;
default:
resultState = NullableFlowState.NotNull;
break;
}
}
if (operatorKind.IsLifted() && !operatorKind.IsComparison())
{
resultState = leftType.State.Join(rightType.State);
}
return TypeWithState.Create(resultType, resultState);
}
protected override void AfterLeftChildHasBeenVisited(BoundBinaryOperator binary)
{
Debug.Assert(!IsConditionalState);
TypeWithState leftType = ResultType;
var rightType = VisitRvalueWithState(binary.Right);
Debug.Assert(!IsConditionalState);
// At this point, State.Reachable may be false for
// invalid code such as `s + throw new Exception()`.
if (binary.OperatorKind.IsUserDefined() && binary.MethodOpt?.ParameterCount == 2)
{
var parameters = binary.MethodOpt.Parameters;
ReportArgumentWarnings(binary.Left, leftType, parameters[0]);
ReportArgumentWarnings(binary.Right, rightType, parameters[1]);
}
Debug.Assert(!IsConditionalState);
// For nested binary operators, this can be the only time they're visited due to explicit stack used in AbstractFlowPass.VisitBinaryOperator,
// so we need to set the flow-analyzed type here.
var inferredResult = InferResultNullability(binary, leftType, rightType);
SetResult(binary, inferredResult, inferredResult.ToTypeWithAnnotations());
BinaryOperatorKind op = binary.OperatorKind.Operator();
// learn from non-null constant
BoundExpression operandComparedToNonNull = null;
if (isNonNullConstant(binary.Left))
{
operandComparedToNonNull = binary.Right;
}
else if (isNonNullConstant(binary.Right))
{
operandComparedToNonNull = binary.Left;
}
if (operandComparedToNonNull != null)
{
switch (op)
{
case BinaryOperatorKind.Equal:
case BinaryOperatorKind.GreaterThan:
case BinaryOperatorKind.LessThan:
case BinaryOperatorKind.GreaterThanOrEqual:
case BinaryOperatorKind.LessThanOrEqual:
operandComparedToNonNull = SkipReferenceConversions(operandComparedToNonNull);
splitAndLearnFromNonNullTest(operandComparedToNonNull, whenTrue: true);
return;
case BinaryOperatorKind.NotEqual:
operandComparedToNonNull = SkipReferenceConversions(operandComparedToNonNull);
splitAndLearnFromNonNullTest(operandComparedToNonNull, whenTrue: false);
return;
default:
break;
};
}
// learn from null constant
if (op == BinaryOperatorKind.Equal || op == BinaryOperatorKind.NotEqual)
{
BoundExpression operandComparedToNull = null;
if (binary.Right.ConstantValue?.IsNull == true)
{
operandComparedToNull = binary.Left;
}
else if (binary.Left.ConstantValue?.IsNull == true)
{
operandComparedToNull = binary.Right;
}
if (operandComparedToNull != null)
{
operandComparedToNull = SkipReferenceConversions(operandComparedToNull);
// Set all nested conditional slots. For example in a?.b?.c we'll set a, b, and c.
bool nonNullCase = op != BinaryOperatorKind.Equal; // true represents WhenTrue
splitAndLearnFromNonNullTest(operandComparedToNull, whenTrue: nonNullCase);
// `x == null` and `x != null` are pure null tests so update the null-state in the alternative branch too
LearnFromNullTest(operandComparedToNull, ref nonNullCase ? ref StateWhenFalse : ref StateWhenTrue);
}
}
static BoundExpression skipImplicitNullableConversions(BoundExpression possiblyConversion)
{
while (possiblyConversion.Kind == BoundKind.Conversion &&
possiblyConversion is BoundConversion { ConversionKind: ConversionKind.ImplicitNullable, Operand: var operand })
{
possiblyConversion = operand;
}
return possiblyConversion;
}
void splitAndLearnFromNonNullTest(BoundExpression operandComparedToNull, bool whenTrue)
{
var slotBuilder = ArrayBuilder<int>.GetInstance();
GetSlotsToMarkAsNotNullable(operandComparedToNull, slotBuilder);
if (slotBuilder.Count != 0)
{
Split();
ref LocalState stateToUpdate = ref whenTrue ? ref this.StateWhenTrue : ref this.StateWhenFalse;
MarkSlotsAsNotNull(slotBuilder, ref stateToUpdate);
}
slotBuilder.Free();
}
static bool isNonNullConstant(BoundExpression expr)
=> skipImplicitNullableConversions(expr).ConstantValue?.IsNull == false;
}
/// <summary>
/// If we learn that the operand is non-null, we can infer that certain
/// sub-expressions were also non-null.
/// Get all nested conditional slots for those sub-expressions. For example in a?.b?.c we'll set a, b, and c.
/// Only returns slots for tracked expressions.
/// </summary>
private void GetSlotsToMarkAsNotNullable(BoundExpression operand, ArrayBuilder<int> slotBuilder)
{
Debug.Assert(operand != null);
var previousConditionalAccessSlot = _lastConditionalAccessSlot;
try
{
while (true)
{
// Due to the nature of binding, if there are conditional access they will be at the top of the bound tree,
// potentially with a conversion on top of it. We go through any conditional accesses, adding slots for the
// conditional receivers if they have them. If we ever get to a receiver that MakeSlot doesn't return a slot
// for, nothing underneath is trackable and we bail at that point. Example:
//
// a?.GetB()?.C // a is a field, GetB is a method, and C is a property
//
// The top of the tree is the a?.GetB() conditional call. We'll ask for a slot for a, and we'll get one because
// fields have slots. The AccessExpression of the BoundConditionalAccess is another BoundConditionalAccess, this time
// with a receiver of the GetB() BoundCall. Attempting to get a slot for this receiver will fail, and we'll
// return an array with just the slot for a.
int slot;
switch (operand.Kind)
{
case BoundKind.Conversion:
// https://github.com/dotnet/roslyn/issues/33879 Detect when conversion has a nullable operand
operand = ((BoundConversion)operand).Operand;
continue;
case BoundKind.ConditionalAccess:
var conditional = (BoundConditionalAccess)operand;
slot = MakeSlot(conditional.Receiver);
if (slot > 0)
{
// We need to continue the walk regardless of whether the receiver should be updated.
var receiverType = conditional.Receiver.Type;
if (PossiblyNullableType(receiverType))
{
slotBuilder.Add(slot);
}
if (receiverType.IsNullableType())
{
slot = GetNullableOfTValueSlot(receiverType, slot, out _);
}
}
if (slot > 0)
{
// When MakeSlot is called on the nested AccessExpression, it will recurse through receivers
// until it gets to the BoundConditionalReceiver associated with this node. In our override,
// we substitute this slot when we encounter a BoundConditionalReceiver, and reset the
// _lastConditionalAccess field.
_lastConditionalAccessSlot = slot;
operand = conditional.AccessExpression;
continue;
}
// If there's no slot for this receiver, there cannot be another slot for any of the remaining
// access expressions.
break;
default:
// Attempt to create a slot for the current thing. If there were any more conditional accesses,
// they would have been on top, so this is the last thing we need to specially handle.
// https://github.com/dotnet/roslyn/issues/33879 When we handle unconditional access survival (ie after
// c.D has been invoked, c must be nonnull or we've thrown a NullRef), revisit whether
// we need more special handling here
slot = MakeSlot(operand);
if (slot > 0 && PossiblyNullableType(operand.Type))
{
slotBuilder.Add(slot);
}
break;
}
return;
}
}
finally
{
_lastConditionalAccessSlot = previousConditionalAccessSlot;
}
}
private static bool PossiblyNullableType(TypeSymbol operandType) => operandType?.CanContainNull() == true;
private static void MarkSlotsAsNotNull(ArrayBuilder<int> slots, ref LocalState stateToUpdate)
{
if (slots is null)
{
return;
}
foreach (int slot in slots)
{
stateToUpdate[slot] = NullableFlowState.NotNull;
}
}
private void LearnFromNonNullTest(BoundExpression expression, ref LocalState state)
{
var slotBuilder = ArrayBuilder<int>.GetInstance();
GetSlotsToMarkAsNotNullable(expression, slotBuilder);
MarkSlotsAsNotNull(slotBuilder, ref state);
slotBuilder.Free();
}
private void LearnFromNonNullTest(int slot, ref LocalState state)
{
state[slot] = NullableFlowState.NotNull;
}
private int LearnFromNullTest(BoundExpression expression, ref LocalState state)
{
var expressionWithoutConversion = RemoveConversion(expression, includeExplicitConversions: true).expression;
var slot = MakeSlot(expressionWithoutConversion);
return LearnFromNullTest(slot, expressionWithoutConversion.Type, ref state);
}
private int LearnFromNullTest(int slot, TypeSymbol expressionType, ref LocalState state)
{
if (slot > 0 && PossiblyNullableType(expressionType))
{
state[slot] = NullableFlowState.MaybeNull;
}
return slot;
}
private static BoundExpression SkipReferenceConversions(BoundExpression possiblyConversion)
{
while (possiblyConversion.Kind == BoundKind.Conversion)
{
var conversion = (BoundConversion)possiblyConversion;
switch (conversion.ConversionKind)
{
case ConversionKind.ImplicitReference:
case ConversionKind.ExplicitReference:
possiblyConversion = conversion.Operand;
break;
default:
return possiblyConversion;
}
}
return possiblyConversion;
}
public override BoundNode VisitNullCoalescingAssignmentOperator(BoundNullCoalescingAssignmentOperator node)
{
BoundExpression leftOperand = node.LeftOperand;
BoundExpression rightOperand = node.RightOperand;
int leftSlot = MakeSlot(leftOperand);
// The assignment to the left below needs the declared type from VisitLvalue, but the hidden
// unnecessary check diagnostic needs the current adjusted type of the slot
TypeWithAnnotations targetType = VisitLvalueWithAnnotations(leftOperand);
var leftState = this.State.Clone();
LearnFromNonNullTest(leftOperand, ref leftState);
LearnFromNullTest(leftOperand, ref this.State);
TypeWithState rightResult = VisitOptionalImplicitConversion(rightOperand, targetType, useLegacyWarnings: UseLegacyWarnings(leftOperand, targetType), trackMembers: false, AssignmentKind.Assignment);
TrackNullableStateForAssignment(rightOperand, targetType, leftSlot, rightResult, MakeSlot(rightOperand));
Join(ref this.State, ref leftState);
TypeWithState resultType = GetNullCoalescingResultType(rightResult, targetType.Type);
SetResultType(node, resultType);
return null;
}
public override BoundNode VisitNullCoalescingOperator(BoundNullCoalescingOperator node)
{
Debug.Assert(!IsConditionalState);
BoundExpression leftOperand = node.LeftOperand;
BoundExpression rightOperand = node.RightOperand;
TypeWithState leftResult = VisitRvalueWithState(leftOperand);
TypeWithState rightResult;
if (IsConstantNull(leftOperand))
{
rightResult = VisitRvalueWithState(rightOperand);
// Should be able to use rightResult for the result of the operator but
// binding may have generated a different result type in the case of errors.
SetResultType(node, TypeWithState.Create(node.Type, rightResult.State));
return null;
}
var whenNotNull = this.State.Clone();
LearnFromNonNullTest(leftOperand, ref whenNotNull);
LearnFromNullTest(leftOperand, ref this.State);
bool leftIsConstant = leftOperand.ConstantValue != null;
if (leftIsConstant)
{
SetUnreachable();
}
// https://github.com/dotnet/roslyn/issues/29955 For cases where the left operand determines
// the type, we should unwrap the right conversion and re-apply.
rightResult = VisitRvalueWithState(rightOperand);
Join(ref this.State, ref whenNotNull);
TypeSymbol resultType;
var leftResultType = leftResult.Type;
var rightResultType = rightResult.Type;
switch (node.OperatorResultKind)
{
case BoundNullCoalescingOperatorResultKind.NoCommonType:
resultType = node.Type;
break;
case BoundNullCoalescingOperatorResultKind.LeftType:
resultType = getLeftResultType(leftResultType, rightResultType);
break;
case BoundNullCoalescingOperatorResultKind.LeftUnwrappedType:
resultType = getLeftResultType(leftResultType.StrippedType(), rightResultType);
break;
case BoundNullCoalescingOperatorResultKind.RightType:
resultType = getRightResultType(leftResultType, rightResultType);
break;
case BoundNullCoalescingOperatorResultKind.LeftUnwrappedRightType:
resultType = getRightResultType(leftResultType.StrippedType(), rightResultType);
break;
case BoundNullCoalescingOperatorResultKind.RightDynamicType:
resultType = rightResultType;
break;
default:
throw ExceptionUtilities.UnexpectedValue(node.OperatorResultKind);
}
SetResultType(node, GetNullCoalescingResultType(rightResult, resultType));
return null;
TypeSymbol getLeftResultType(TypeSymbol leftType, TypeSymbol rightType)
{
Debug.Assert(!(rightType is null));
// If there was an identity conversion between the two operands (in short, if there
// is no implicit conversion on the right operand), then check nullable conversions
// in both directions since it's possible the right operand is the better result type.
if ((node.RightOperand as BoundConversion)?.ExplicitCastInCode != false &&
GenerateConversionForConditionalOperator(node.LeftOperand, leftType, rightType, reportMismatch: false).Exists)
{
return rightType;
}
GenerateConversionForConditionalOperator(node.RightOperand, rightType, leftType, reportMismatch: true);
return leftType;
}
TypeSymbol getRightResultType(TypeSymbol leftType, TypeSymbol rightType)
{
GenerateConversionForConditionalOperator(node.LeftOperand, leftType, rightType, reportMismatch: true);
return rightType;
}
}
/// <summary>
/// Return top-level nullability for the expression. This method should be called on a limited
/// set of expressions only. It should not be called on expressions tracked by flow analysis
/// other than <see cref="BoundKind.ExpressionWithNullability"/> which is an expression
/// specifically created in NullableWalker to represent the flow analysis state.
/// </summary>
private static NullableAnnotation GetNullableAnnotation(BoundExpression expr)
{
switch (expr.Kind)
{
case BoundKind.DefaultExpression:
case BoundKind.Literal:
return (expr.ConstantValue?.IsNull != false) ? NullableAnnotation.NotAnnotated : NullableAnnotation.Annotated;
case BoundKind.ExpressionWithNullability:
return ((BoundExpressionWithNullability)expr).NullableAnnotation;
case BoundKind.MethodGroup:
case BoundKind.UnboundLambda:
return NullableAnnotation.NotAnnotated;
default:
Debug.Assert(false); // unexpected value
return NullableAnnotation.Oblivious;
}
}
private static TypeWithState GetNullCoalescingResultType(TypeWithState rightResult, TypeSymbol resultType)
{
NullableFlowState resultState = rightResult.State;
return TypeWithState.Create(resultType, resultState);
}
public override BoundNode VisitConditionalAccess(BoundConditionalAccess node)
{
Debug.Assert(!IsConditionalState);
var receiver = node.Receiver;
var receiverType = VisitRvalueWithState(receiver);
_currentConditionalReceiverVisitResult = _visitResult;
var previousConditionalAccessSlot = _lastConditionalAccessSlot;
var receiverState = this.State.Clone();
if (IsConstantNull(node.Receiver))
{
SetUnreachable();
_lastConditionalAccessSlot = -1;
}
else
{
// In the when-null branch, the receiver is known to be maybe-null.
// In the other branch, the receiver is known to be non-null.
_lastConditionalAccessSlot = LearnFromNullTest(receiver, ref receiverState);
LearnFromNonNullTest(receiver, ref this.State);
}
var accessTypeWithAnnotations = VisitLvalueWithAnnotations(node.AccessExpression);
TypeSymbol accessType = accessTypeWithAnnotations.Type;
Join(ref this.State, ref receiverState);
var oldType = node.Type;
var resultType =
oldType.IsVoidType() || oldType.IsErrorType() ? oldType :
oldType.IsNullableType() && !accessType.IsNullableType() ? MakeNullableOf(accessTypeWithAnnotations) :
accessType;
// If the result type does not allow annotations, then we produce a warning because
// the result may be null.
if (RequiresSafetyWarningWhenNullIntroduced(resultType))
{
ReportDiagnostic(ErrorCode.WRN_ConditionalAccessMayReturnNull, node.Syntax, accessType);
}
// Per LDM 2019-02-13 decision, the result of a conditional access "may be null" even if
// both the receiver and right-hand-side are believed not to be null.
SetResultType(node, TypeWithState.Create(resultType, NullableFlowState.MaybeNull));
_currentConditionalReceiverVisitResult = default;
_lastConditionalAccessSlot = previousConditionalAccessSlot;
return null;
}
public override BoundNode VisitConditionalOperator(BoundConditionalOperator node)
{
VisitCondition(node.Condition);
var consequenceState = this.StateWhenTrue;
var alternativeState = this.StateWhenFalse;
TypeWithState consequenceRValue;
TypeWithState alternativeRValue;
if (node.IsRef)
{
TypeWithAnnotations consequenceLValue;
TypeWithAnnotations alternativeLValue;
(consequenceLValue, consequenceRValue) = visitConditionalRefOperand(consequenceState, node.Consequence);
consequenceState = this.State;
(alternativeLValue, alternativeRValue) = visitConditionalRefOperand(alternativeState, node.Alternative);
Join(ref this.State, ref consequenceState);
TypeSymbol refResultType = node.Type.SetUnknownNullabilityForReferenceTypes();
if (IsNullabilityMismatch(consequenceLValue, alternativeLValue))
{
// l-value types must match
ReportNullabilityMismatchInAssignment(node.Syntax, consequenceLValue, alternativeLValue);
}
else if (!node.HasErrors)
{
refResultType = consequenceRValue.Type.MergeNullability(alternativeRValue.Type, VarianceKind.None);
}
var lValueAnnotation = consequenceLValue.NullableAnnotation.EnsureCompatible(alternativeLValue.NullableAnnotation);
var rValueState = consequenceRValue.State.Join(alternativeRValue.State);
SetResult(node, TypeWithState.Create(refResultType, rValueState), TypeWithAnnotations.Create(refResultType, lValueAnnotation));
return null;
}
BoundExpression consequence;
BoundExpression alternative;
Conversion consequenceConversion;
Conversion alternativeConversion;
bool consequenceEndReachable;
bool alternativeEndReachable;
// In cases where one branch is unreachable, we don't need to Unsplit the state
if (!alternativeState.Reachable)
{
(alternative, alternativeConversion, alternativeRValue) = visitConditionalOperand(alternativeState, node.Alternative);
(consequence, consequenceConversion, consequenceRValue) = visitConditionalOperand(consequenceState, node.Consequence);
alternativeEndReachable = false;
consequenceEndReachable = IsReachable();
}
else if (!consequenceState.Reachable)
{
(consequence, consequenceConversion, consequenceRValue) = visitConditionalOperand(consequenceState, node.Consequence);
(alternative, alternativeConversion, alternativeRValue) = visitConditionalOperand(alternativeState, node.Alternative);
consequenceEndReachable = false;
alternativeEndReachable = IsReachable();
}
else
{
(consequence, consequenceConversion, consequenceRValue) = visitConditionalOperand(consequenceState, node.Consequence);
Unsplit();
consequenceState = this.State;
consequenceEndReachable = consequenceState.Reachable;
(alternative, alternativeConversion, alternativeRValue) = visitConditionalOperand(alternativeState, node.Alternative);
Unsplit();
alternativeEndReachable = this.State.Reachable;
Join(ref this.State, ref consequenceState);
}
TypeSymbol resultType;
if (node.HasErrors)
{
resultType = null;
}
else
{
// Determine nested nullability using BestTypeInferrer.
// If a branch is unreachable, we could use the nested nullability of the other
// branch, but that requires using the nullability of the branch as it applies to the
// target type. For instance, the result of the conditional in the following should
// be `IEnumerable<object>` not `object[]`:
// object[] a = ...;
// IEnumerable<object?> b = ...;
// var c = true ? a : b;
BoundExpression consequencePlaceholder = CreatePlaceholderIfNecessary(consequence, consequenceRValue.ToTypeWithAnnotations());
BoundExpression alternativePlaceholder = CreatePlaceholderIfNecessary(alternative, alternativeRValue.ToTypeWithAnnotations());
HashSet<DiagnosticInfo> useSiteDiagnostics = null;
resultType = BestTypeInferrer.InferBestTypeForConditionalOperator(consequencePlaceholder, alternativePlaceholder, _conversions, out _, ref useSiteDiagnostics);
}
NullableFlowState resultState;
if (resultType is null)
{
resultType = node.Type.SetUnknownNullabilityForReferenceTypes();
resultState = NullableFlowState.NotNull;
var resultTypeWithState = TypeWithState.Create(resultType, resultState);
if (consequence != node.Consequence)
{
TrackAnalyzedNullabilityThroughConversionGroup(resultTypeWithState, (BoundConversion)node.Consequence, consequence);
}
if (alternative != node.Alternative)
{
TrackAnalyzedNullabilityThroughConversionGroup(resultTypeWithState, (BoundConversion)node.Alternative, alternative);
}
}
else
{
var resultTypeWithAnnotations = TypeWithAnnotations.Create(resultType);
TypeWithState convertedConsequenceResult = convertResult(
node.Consequence,
consequence,
consequenceConversion,
resultTypeWithAnnotations,
consequenceRValue,
consequenceEndReachable);
TypeWithState convertedAlternativeResult = convertResult(
node.Alternative,
alternative,
alternativeConversion,
resultTypeWithAnnotations,
alternativeRValue,
alternativeEndReachable);
resultState = convertedConsequenceResult.State.Join(convertedAlternativeResult.State);
}
SetResultType(node, TypeWithState.Create(resultType, resultState));
return null;
(BoundExpression, Conversion, TypeWithState) visitConditionalOperand(LocalState state, BoundExpression operand)
{
Conversion conversion;
SetState(state);
Debug.Assert(!node.IsRef);
BoundExpression operandNoConversion;
(operandNoConversion, conversion) = RemoveConversion(operand, includeExplicitConversions: false);
Visit(operandNoConversion);
return (operandNoConversion, conversion, ResultType);
}
(TypeWithAnnotations LValueType, TypeWithState RValueType) visitConditionalRefOperand(LocalState state, BoundExpression operand)
{
SetState(state);
Debug.Assert(node.IsRef);
TypeWithAnnotations lValueType = VisitLvalueWithAnnotations(operand);
return (lValueType, ResultType);
}
TypeWithState convertResult(
BoundExpression node,
BoundExpression operand,
Conversion conversion,
TypeWithAnnotations targetType,
TypeWithState operandType,
bool isReachable)
{
bool previousDisabledDiagnostics = _disableDiagnostics;
// If the node is not reachable, then we're only visiting to get
// nullability information for the public API, and not to produce diagnostics.
// Disable diagnostics, and return default for the resulting state
// to indicate that warnings were suppressed.
if (!isReachable)
{
_disableDiagnostics = true;
}
var resultType = VisitConversion(
GetConversionIfApplicable(node, operand),
operand,
conversion,
targetType,
operandType,
checkConversion: true,
fromExplicitCast: false,
useLegacyWarnings: false,
AssignmentKind.Assignment,
reportTopLevelWarnings: false);
if (!isReachable)
{
resultType = default;
_disableDiagnostics = previousDisabledDiagnostics;
}
return resultType;
}
}
bool IsReachable()
=> this.IsConditionalState ? (this.StateWhenTrue.Reachable || this.StateWhenFalse.Reachable) : this.State.Reachable;
/// <summary>
/// Placeholders are bound expressions with type and state.
/// But for typeless expressions (such as `null` or `(null, null)` we hold onto the original bound expression,
/// as it will be useful for conversions from expression.
/// </summary>
private static BoundExpression CreatePlaceholderIfNecessary(BoundExpression expr, TypeWithAnnotations type)
{
return !type.HasType ?
expr :
new BoundExpressionWithNullability(expr.Syntax, expr, type.NullableAnnotation, type.Type);
}
public override BoundNode VisitConditionalReceiver(BoundConditionalReceiver node)
{
var rvalueType = _currentConditionalReceiverVisitResult.RValueType.Type;
if (rvalueType?.IsNullableType() == true)
{
rvalueType = rvalueType.GetNullableUnderlyingType();
}
SetResultType(node, TypeWithState.Create(rvalueType, NullableFlowState.NotNull));
return null;
}
public override BoundNode VisitCall(BoundCall node)
{
// Note: we analyze even omitted calls
TypeWithState receiverType = VisitCallReceiver(node);
ReinferMethodAndVisitArguments(node, receiverType);
return null;
}
private void ReinferMethodAndVisitArguments(BoundCall node, TypeWithState receiverType)
{
// https://github.com/dotnet/roslyn/issues/29605 Can we handle some error cases?
// (Compare with CSharpOperationFactory.CreateBoundCallOperation.)
var method = node.Method;
ImmutableArray<RefKind> refKindsOpt = node.ArgumentRefKindsOpt;
if (!receiverType.HasNullType)
{
// Update method based on inferred receiver type.
method = (MethodSymbol)AsMemberOfType(receiverType.Type, method);
}
method = VisitArguments(node, node.Arguments, refKindsOpt, method.Parameters, node.ArgsToParamsOpt,
node.Expanded, node.InvokedAsExtensionMethod, method).method;
if (method.MethodKind == MethodKind.LocalFunction)
{
var localFunc = (LocalFunctionSymbol)method.OriginalDefinition;
ReplayReadsAndWrites(localFunc, node.Syntax, writes: true);
}
var type = method.ReturnTypeWithAnnotations;
SetLvalueResultType(node, type);
}
private TypeWithState VisitCallReceiver(BoundCall node)
{
var receiverOpt = node.ReceiverOpt;
TypeWithState receiverType = default;
if (receiverOpt != null)
{
receiverType = VisitRvalueWithState(receiverOpt);
// methods which are members of Nullable<T> (ex: ToString, GetHashCode) can be invoked on null receiver.
// However, inherited methods (ex: GetType) are invoked on a boxed value (since base types are reference types)
// and therefore in those cases nullable receivers should be checked for nullness.
bool checkNullableValueType = false;
var type = receiverType.Type;
var method = node.Method;
if (!method.IsStatic &&
type?.IsNullableType() == true &&
method.ContainingType.IsReferenceType)
{
checkNullableValueType = true;
}
else if (method.OriginalDefinition == compilation.GetSpecialTypeMember(SpecialMember.System_Nullable_T_get_Value))
{
// call to get_Value may not occur directly in source, but may be inserted as a result of premature lowering.
// One example where we do it is foreach with nullables.
// The reason is Dev10 compatibility (see: UnwrapCollectionExpressionIfNullable in ForEachLoopBinder.cs)
// Regardless of the reasons, we know that the method does not tolerate nulls.
checkNullableValueType = true;
}
// https://github.com/dotnet/roslyn/issues/30598: Mark receiver as not null
// after arguments have been visited, and only if the receiver has not changed.
_ = CheckPossibleNullReceiver(receiverOpt, checkNullableValueType);
}
return receiverType;
}
/// <summary>
/// For each argument, figure out if its corresponding parameter is annotated with NotNullWhenFalse or
/// EnsuresNotNull.
/// </summary>
private static ImmutableArray<FlowAnalysisAnnotations> GetAnnotations(int numArguments,
bool expanded, ImmutableArray<ParameterSymbol> parameters, ImmutableArray<int> argsToParamsOpt)
{
ArrayBuilder<FlowAnalysisAnnotations> builder = null;
for (int i = 0; i < numArguments; i++)
{
(ParameterSymbol parameter, _) = GetCorrespondingParameter(i, parameters, argsToParamsOpt, expanded);
FlowAnalysisAnnotations annotations = parameter?.FlowAnalysisAnnotations ?? FlowAnalysisAnnotations.None;
annotations = removeInapplicableAnnotations(parameter, annotations);
if (annotations != FlowAnalysisAnnotations.None && builder == null)
{
builder = ArrayBuilder<FlowAnalysisAnnotations>.GetInstance(numArguments);
builder.AddMany(FlowAnalysisAnnotations.None, i);
}
if (builder != null)
{
builder.Add(annotations);
}
}
return builder == null ? default : builder.ToImmutableAndFree();
FlowAnalysisAnnotations removeInapplicableAnnotations(ParameterSymbol parameter, FlowAnalysisAnnotations annotations)
{
// Ignore NotNullWhenTrue that is inapplicable
annotations = removeInapplicableNotNullWhenSense(parameter, annotations, sense: true);
// Ignore NotNullWhenFalse that is inapplicable
annotations = removeInapplicableNotNullWhenSense(parameter, annotations, sense: false);
const FlowAnalysisAnnotations both = FlowAnalysisAnnotations.AssertsTrue | FlowAnalysisAnnotations.AssertsFalse;
if (parameter?.Type.SpecialType != SpecialType.System_Boolean)
{
// AssertsTrue and AssertsFalse must be applied to a bool parameter
annotations &= ~both;
}
else if ((annotations & both) == both)
{
// We'll ignore AssertsTrue and AssertsFalse if both set
annotations &= ~both;
}
return annotations;
}
FlowAnalysisAnnotations removeInapplicableNotNullWhenSense(ParameterSymbol parameter, FlowAnalysisAnnotations annotations, bool sense)
{
if (parameter is null)
{
return annotations;
}
var whenSense = sense ? FlowAnalysisAnnotations.NotNullWhenTrue : FlowAnalysisAnnotations.NotNullWhenFalse;
var whenNotSense = sense ? FlowAnalysisAnnotations.NotNullWhenFalse : FlowAnalysisAnnotations.NotNullWhenTrue;
// NotNullWhenSense (without NotNullWhenNotSense) must be applied on a bool-returning member
if ((annotations & whenSense) != 0 &&
(annotations & whenNotSense) == 0 &&
parameter.ContainingSymbol.GetTypeOrReturnType().SpecialType != SpecialType.System_Boolean)
{
annotations &= ~whenSense;
}
// NotNullWhenSense must be applied to a reference type, a nullable value type, or an unconstrained generic type
if ((annotations & whenSense) != 0 && !parameter.Type.CanContainNull())
{
annotations &= ~whenSense;
}
// NotNullWhenSense is inapplicable when argument corresponds to params parameter and we're in expanded form
if ((annotations & whenSense) != 0 && expanded && ReferenceEquals(parameter, parameters.Last()))
{
annotations &= ~whenSense;
}
return annotations;
}
}
// https://github.com/dotnet/roslyn/issues/29863 Record in the node whether type
// arguments were implicit, to allow for cases where the syntax is not an
// invocation (such as a synthesized call from a query interpretation).
private static bool HasImplicitTypeArguments(BoundExpression node)
{
var syntax = node.Syntax;
if (syntax.Kind() != SyntaxKind.InvocationExpression)
{
// Unexpected syntax kind.
return false;
}
return HasImplicitTypeArguments(((InvocationExpressionSyntax)syntax).Expression);
}
private static bool HasImplicitTypeArguments(SyntaxNode syntax)
{
var nameSyntax = Binder.GetNameSyntax(syntax, out _);
if (nameSyntax == null)
{
// Unexpected syntax kind.
return false;
}
nameSyntax = nameSyntax.GetUnqualifiedName();
return nameSyntax.Kind() != SyntaxKind.GenericName;
}
protected override void VisitArguments(ImmutableArray<BoundExpression> arguments, ImmutableArray<RefKind> refKindsOpt, MethodSymbol method)
{
// Callers should be using VisitArguments overload below.
throw ExceptionUtilities.Unreachable;
}
private ImmutableArray<VisitArgumentResult> VisitArguments(
BoundExpression node,
ImmutableArray<BoundExpression> arguments,
ImmutableArray<RefKind> refKindsOpt,
MethodSymbol method,
ImmutableArray<int> argsToParamsOpt,
bool expanded)
{
return VisitArguments(node, arguments, refKindsOpt, method is null ? default : method.Parameters, argsToParamsOpt, expanded, invokedAsExtensionMethod: false).results;
}
private ImmutableArray<VisitArgumentResult> VisitArguments(
BoundExpression node,
ImmutableArray<BoundExpression> arguments,
ImmutableArray<RefKind> refKindsOpt,
PropertySymbol property,
ImmutableArray<int> argsToParamsOpt,
bool expanded)
{
return VisitArguments(node, arguments, refKindsOpt, property is null ? default : property.Parameters, argsToParamsOpt, expanded, invokedAsExtensionMethod: false).results;
}
/// <summary>
/// If you pass in a method symbol, its type arguments will be re-inferred and the re-inferred method will be returned.
/// </summary>
private (MethodSymbol method, ImmutableArray<VisitArgumentResult> results) VisitArguments(
BoundExpression node,
ImmutableArray<BoundExpression> arguments,
ImmutableArray<RefKind> refKindsOpt,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<int> argsToParamsOpt,
bool expanded,
bool invokedAsExtensionMethod,
MethodSymbol method = null)
{
Debug.Assert(!arguments.IsDefault);
var savedState = this.State.Clone();
(ImmutableArray<BoundExpression> argumentsNoConversions, ImmutableArray<Conversion> conversions) = RemoveArgumentConversions(arguments, refKindsOpt);
// We do a first pass to work through the arguments without making any assumptions
ImmutableArray<VisitArgumentResult> results = VisitArgumentsEvaluate(argumentsNoConversions, refKindsOpt);
if ((object)method != null && method.IsGenericMethod)
{
if (HasImplicitTypeArguments(node))
{
method = InferMethodTypeArguments((BoundCall)node, method, GetArgumentsForMethodTypeInference(argumentsNoConversions, results));
parameters = method.Parameters;
}
if (ConstraintsHelper.RequiresChecking(method))
{
var syntax = node.Syntax;
CheckMethodConstraints((syntax as InvocationExpressionSyntax)?.Expression ?? syntax, method);
}
}
if (!node.HasErrors && !parameters.IsDefault)
{
VisitArgumentConversions(arguments, argumentsNoConversions, conversions, refKindsOpt, parameters, argsToParamsOpt, expanded, invokedAsExtensionMethod, results);
}
// We do a second pass through the arguments, ignoring any diagnostics produced, but honoring the annotations,
// to get the proper result state. Annotations are ignored when binding an attribute to avoid cycles.
// (Additional warnings are only expected in error scenarios, particularly calling a method in an attribute argument.)
ImmutableArray<FlowAnalysisAnnotations> annotations =
(this.methodMainNode.Kind == BoundKind.Attribute) ?
default :
GetAnnotations(argumentsNoConversions.Length, expanded, parameters, argsToParamsOpt);
if (!annotations.IsDefault)
{
this.SetState(savedState);
bool saveDisableDiagnostics = _disableDiagnostics;
_disableDiagnostics = true;
if (!node.HasErrors && !parameters.IsDefault)
{
// recompute out vars after state was reset
VisitArgumentConversions(arguments, argumentsNoConversions, conversions, refKindsOpt, parameters, argsToParamsOpt, expanded, invokedAsExtensionMethod, results);
}
VisitArgumentsEvaluateHonoringAnnotations(argumentsNoConversions, refKindsOpt, annotations);
_disableDiagnostics = saveDisableDiagnostics;
}
return (method, results);
}
private ImmutableArray<VisitArgumentResult> VisitArgumentsEvaluate(ImmutableArray<BoundExpression> arguments, ImmutableArray<RefKind> refKindsOpt)
{
Debug.Assert(!IsConditionalState);
int n = arguments.Length;
if (n == 0)
{
return ImmutableArray<VisitArgumentResult>.Empty;
}
var builder = ArrayBuilder<VisitArgumentResult>.GetInstance(n);
for (int i = 0; i < n; i++)
{
builder.Add(VisitArgumentEvaluate(arguments[i], GetRefKind(refKindsOpt, i), preserveConditionalState: false));
}
SetInvalidResult();
return builder.ToImmutableAndFree();
}
private VisitArgumentResult VisitArgumentEvaluate(BoundExpression argument, RefKind refKind, bool preserveConditionalState)
{
Debug.Assert(!IsConditionalState);
var savedState = (argument.Kind == BoundKind.Lambda) ? this.State.Clone() : default(Optional<LocalState>);
switch (refKind)
{
case RefKind.Ref:
Visit(argument);
if (!preserveConditionalState)
{
Unsplit();
}
break;
case RefKind.None:
case RefKind.In:
if (preserveConditionalState)
{
Visit(argument);
// No Unsplit
UseRvalueOnly(argument); // force use of flow result
}
else
{
VisitRvalue(argument);
}
break;
case RefKind.Out:
// As far as we can tell, there is no scenario relevant to nullability analysis
// where splitting an L-value (for instance with a ref conditional) would affect the result.
Visit(argument);
// We'll want to use the l-value type, rather than the result type, for method re-inference
UseLvalueOnly(argument);
break;
}
return new VisitArgumentResult(_visitResult, savedState);
}
/// <summary>
/// Visit all the arguments for the purpose of computing the exit state of the method,
/// given the annotations.
/// If there is any [NotNullWhenTrue/False] annotation, then we'll return in a conditional state for the invocation.
/// </summary>
private void VisitArgumentsEvaluateHonoringAnnotations(
ImmutableArray<BoundExpression> arguments,
ImmutableArray<RefKind> refKindsOpt,
ImmutableArray<FlowAnalysisAnnotations> annotations)
{
Debug.Assert(!IsConditionalState);
Debug.Assert(annotations.Length == arguments.Length);
Debug.Assert(_disableDiagnostics);
for (int i = 0; i < arguments.Length; i++)
{
FlowAnalysisAnnotations annotation = annotations[i];
bool assertsTrue = (annotation & FlowAnalysisAnnotations.AssertsTrue) != 0;
bool assertsFalse = (annotation & FlowAnalysisAnnotations.AssertsFalse) != 0;
if (this.IsConditionalState)
{
// We could be in a conditional state because of a conditional annotation (like NotNullWhenFalse)
// Then WhenTrue/False states correspond to the invocation returning true/false
// We'll first assume that we're in the unconditional state where the method returns true,
// then we'll repeat assuming the method returns false.
LocalState whenTrue = this.StateWhenTrue.Clone();
LocalState whenFalse = this.StateWhenFalse.Clone();
this.SetState(whenTrue);
visitArgumentEvaluateAndUnsplit(i, assertsTrue, assertsFalse);
Debug.Assert(!IsConditionalState);
whenTrue = this.State; // LocalState may be a struct
this.SetState(whenFalse);
visitArgumentEvaluateAndUnsplit(i, assertsTrue, assertsFalse);
Debug.Assert(!IsConditionalState);
whenFalse = this.State; // LocalState may be a struct
this.SetConditionalState(whenTrue, whenFalse);
}
else
{
visitArgumentEvaluateAndUnsplit(i, assertsTrue, assertsFalse);
}
var argument = arguments[i];
var argumentType = argument.Type;
if (!PossiblyNullableType(argumentType))
{
continue;
}
bool notNullWhenTrue = (annotation & FlowAnalysisAnnotations.NotNullWhenTrue) != 0;
bool notNullWhenFalse = (annotation & FlowAnalysisAnnotations.NotNullWhenFalse) != 0;
if (notNullWhenTrue || notNullWhenFalse)
{
// The WhenTrue/False states correspond to the invocation returning true/false
bool wasPreviouslySplit = this.IsConditionalState;
Split();
var slotBuilder = ArrayBuilder<int>.GetInstance();
GetSlotsToMarkAsNotNullable(arguments[i], slotBuilder);
if (notNullWhenTrue)
{
MarkSlotsAsNotNull(slotBuilder, ref StateWhenTrue);
}
if (notNullWhenFalse)
{
MarkSlotsAsNotNull(slotBuilder, ref StateWhenFalse);
if (notNullWhenTrue && !wasPreviouslySplit) Unsplit();
}
slotBuilder.Free();
}
}
SetInvalidResult();
// Evaluate an argument, potentially producing a split state.
// Then unsplit it based on [AssertsTrue] or [AssertsFalse] attributes, or default Unsplit otherwise.
void visitArgumentEvaluateAndUnsplit(int argumentIndex, bool assertsTrue, bool assertsFalse)
{
Debug.Assert(!IsConditionalState);
VisitArgumentEvaluate(arguments[argumentIndex], GetRefKind(refKindsOpt, argumentIndex), preserveConditionalState: true);
if (!this.IsConditionalState)
{
return;
}
else if (assertsTrue)
{
this.SetState(this.StateWhenTrue);
}
else if (assertsFalse)
{
this.SetState(this.StateWhenFalse);
}
else
{
this.Unsplit();
}
}
}
private void VisitArgumentConversions(
ImmutableArray<BoundExpression> arguments,
ImmutableArray<BoundExpression> argumentsNoConversions,
ImmutableArray<Conversion> conversions,
ImmutableArray<RefKind> refKindsOpt,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<int> argsToParamsOpt,
bool expanded,
bool invokedAsExtensionMethod,
ImmutableArray<VisitArgumentResult> results)
{
for (int i = 0; i < argumentsNoConversions.Length; i++)
{
(ParameterSymbol parameter, TypeWithAnnotations parameterType) = GetCorrespondingParameter(i, parameters, argsToParamsOpt, expanded);
if (parameter is null)
{
continue;
}
var argumentNoConversion = argumentsNoConversions[i];
VisitArgumentConversion(
GetConversionIfApplicable(arguments[i], argumentNoConversion),
argumentNoConversion,
conversions.IsDefault ? Conversion.Identity : conversions[i],
GetRefKind(refKindsOpt, i),
parameter,
parameterType,
results[i],
invokedAsExtensionMethod && i == 0);
}
}
/// <summary>
/// Report warnings for an argument corresponding to a specific parameter.
/// </summary>
private void VisitArgumentConversion(
BoundConversion conversionOpt,
BoundExpression argumentNoConversion,
Conversion conversion,
RefKind refKind,
ParameterSymbol parameter,
TypeWithAnnotations parameterType,
VisitArgumentResult result,
bool extensionMethodThisArgument)
{
// Note: we allow for some variance in `in` and `out` cases. Unlike in binding, we're not
// limited by CLR constraints.
var resultType = result.RValueType;
bool reported = false;
switch (refKind)
{
case RefKind.None:
case RefKind.In:
{
SetResultType(argumentNoConversion,
VisitConversion(
conversionOpt: conversionOpt,
conversionOperand: argumentNoConversion,
conversion: conversion,
targetTypeWithNullability: parameterType,
operandType: resultType,
checkConversion: true,
fromExplicitCast: false,
useLegacyWarnings: false,
assignmentKind: AssignmentKind.Argument,
target: parameter,
extensionMethodThisArgument: extensionMethodThisArgument,
stateForLambda: result.StateForLambda));
}
break;
case RefKind.Ref:
{
if (!argumentNoConversion.IsSuppressed)
{
var lvalueResultType = result.LValueType;
if (IsNullabilityMismatch(lvalueResultType.Type, parameterType.Type))
{
// declared types must match, ignoring top-level nullability
ReportNullabilityMismatchInRefArgument(argumentNoConversion, argumentType: lvalueResultType.Type, parameter, parameterType.Type);
}
else
{
// types match, but state would let a null in
ReportNullableAssignmentIfNecessary(argumentNoConversion, parameterType, resultType, useLegacyWarnings: false);
}
}
// Check assignment from a fictional value from the parameter to the argument.
var parameterWithState = parameterType.ToTypeWithState();
if (argumentNoConversion.IsSuppressed)
{
parameterWithState = parameterWithState.WithNotNullState();
}
var parameterValue = new BoundParameter(argumentNoConversion.Syntax, parameter);
var lValueType = result.LValueType;
TrackNullableStateForAssignment(parameterValue, lValueType, MakeSlot(argumentNoConversion), parameterWithState);
// check whether parameter would unsafely let a null out
ReportNullableAssignmentIfNecessary(parameterValue, lValueType, parameterWithState, useLegacyWarnings: false);
}
break;
case RefKind.Out:
{
var parameterWithState = parameterType.ToTypeWithState();
if (argumentNoConversion is BoundLocal local && local.DeclarationKind == BoundLocalDeclarationKind.WithInferredType)
{
_variableTypes[local.LocalSymbol] = parameterType;
}
var lValueType = result.LValueType;
// Check assignment from a fictional value from the parameter to the argument.
var parameterValue = new BoundParameter(argumentNoConversion.Syntax, parameter);
if (!argumentNoConversion.IsSuppressed && !reported)
{
ReportNullableAssignmentIfNecessary(parameterValue, lValueType, parameterWithState, useLegacyWarnings: UseLegacyWarnings(argumentNoConversion, result.LValueType));
HashSet<DiagnosticInfo> useSiteDiagnostics = null;
if (!_conversions.HasIdentityOrImplicitReferenceConversion(parameterType.Type, lValueType.Type, ref useSiteDiagnostics))
{
ReportNullabilityMismatchInArgument(argumentNoConversion.Syntax, lValueType.Type, parameter, parameterType.Type, forOutput: