// 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 DataFlowPass.
#define REFERENCE_STATE
#endif
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp
{
///
/// Nullability flow analysis.
///
internal sealed partial class NullableWalker : DataFlowPassBase
{
///
/// 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.
///
internal sealed class VariableState
{
// PROTOTYPE(NullableReferenceTypes): Reference the collections directly from the original
// NullableWalker ratherthan coping the collections. (Items are added to the collections
// but never replaced so the collections are lazily populated but otherwise immutable.)
internal readonly ImmutableDictionary VariableSlot;
internal readonly ImmutableArray VariableBySlot;
internal readonly ImmutableDictionary VariableTypes;
internal VariableState(
ImmutableDictionary variableSlot,
ImmutableArray variableBySlot,
ImmutableDictionary variableTypes)
{
VariableSlot = variableSlot;
VariableBySlot = variableBySlot;
VariableTypes = variableTypes;
}
}
///
/// The inferred type at the point of declaration of var locals and parameters.
///
private readonly PooledDictionary _variableTypes = PooledDictionary.GetInstance();
///
/// The current source assembly.
///
private readonly SourceAssemblySymbol _sourceAssembly;
// PROTOTYPE(NullableReferenceTypes): Remove the Binder if possible.
private readonly Binder _binder;
private readonly Conversions _conversions;
///
/// Use the return type and nullability from _methodSignatureOpt to calculate return
/// expression conversions. If false, the signature of _member is used instead.
///
private readonly bool _useMethodSignatureReturnType;
///
/// Use the the parameter types and nullability from _methodSignatureOpt for initial
/// parameter state. If false, the signature of _member is used instead.
///
private readonly bool _useMethodSignatureParameterTypes;
///
/// Method signature used for return type or parameter types. Distinct from _member
/// signature when _member is a lambda and type is inferred from MethodTypeInferrer.
///
private readonly MethodSymbol _methodSignatureOpt;
///
/// Types from return expressions. Used when inferring lambda return type in MethodTypeInferrer.
///
private readonly ArrayBuilder<(RefKind, TypeSymbolWithAnnotations)> _returnTypes;
///
/// An optional callback for callers to receive notification of the inferred type and nullability
/// of each expression in the method. Since the walker may require multiple passes, the callback
/// may be invoked multiple times for a single expression, potentially with different nullability
/// each time. The last call for each expression will include the final inferred type and nullability.
///
private readonly Action _callbackOpt;
///
/// Invalid type, used only to catch Visit methods that do not set
/// _result.Type. See VisitExpressionWithoutStackGuard.
///
private static readonly TypeSymbolWithAnnotations _invalidType = TypeSymbolWithAnnotations.Create(ErrorTypeSymbol.UnknownResultType);
private TypeSymbolWithAnnotations _resultType; // PROTOTYPE(NullableReferenceTypes): Should be return value from the visitor, not mutable state.
///
/// Instances being constructed.
///
private PooledDictionary _placeholderLocals;
///
/// For methods with annotations, we'll need to visit the arguments twice.
/// Once for diagnostics and once for result state (but disabling diagnostics).
///
private bool _disableDiagnostics = false;
///
/// Used to allow to substitute the correct slot for a when
/// it's encountered.
///
private int _lastConditionalAccessSlot = -1;
protected override void Free()
{
_variableTypes.Free();
_placeholderLocals?.Free();
base.Free();
}
private NullableWalker(
CSharpCompilation compilation,
MethodSymbol method,
bool useMethodSignatureReturnType,
bool useMethodSignatureParameterTypes,
MethodSymbol methodSignatureOpt,
BoundNode node,
ArrayBuilder<(RefKind, TypeSymbolWithAnnotations)> returnTypes,
VariableState initialState,
Action callbackOpt)
: base(compilation, method, node, new EmptyStructTypeCache(compilation, dev12CompilerCompatibility: false), trackUnassignments: false)
{
_sourceAssembly = (method is null) ? null : (SourceAssemblySymbol)method.ContainingAssembly;
_callbackOpt = callbackOpt;
// PROTOTYPE(NullableReferenceTypes): Do we really need a Binder?
// If so, are we interested in an InMethodBinder specifically?
_binder = compilation.GetBinderFactory(node.SyntaxTree).GetBinder(node.Syntax);
Debug.Assert(!_binder.Conversions.IncludeNullability);
_conversions = _binder.Conversions.WithNullability(true);
_useMethodSignatureReturnType = (object)methodSignatureOpt != null && useMethodSignatureReturnType;
_useMethodSignatureParameterTypes = (object)methodSignatureOpt != null && useMethodSignatureParameterTypes;
_methodSignatureOpt = methodSignatureOpt;
_returnTypes = returnTypes;
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);
}
}
}
protected override bool ConvertInsufficientExecutionStackExceptionToCancelledByStackGuardException()
{
return true;
}
protected override ImmutableArray Scan(ref bool badRegion)
{
if (_returnTypes != null)
{
_returnTypes.Clear();
}
this.Diagnostics.Clear();
ParameterSymbol methodThisParameter = MethodThisParameter;
this.State = ReachableState(); // entry point is reachable
this.regionPlace = RegionPlace.Before;
EnterParameters(); // with parameters assigned
if ((object)methodThisParameter != null)
{
EnterParameter(methodThisParameter, methodThisParameter.Type);
}
ImmutableArray pendingReturns = base.Scan(ref badRegion);
return pendingReturns;
}
internal static void Analyze(
CSharpCompilation compilation,
MethodSymbol method,
BoundNode node,
DiagnosticBag diagnostics,
Action callbackOpt = null)
{
if (method.IsImplicitlyDeclared)
{
return;
}
Analyze(compilation, method, node, diagnostics, useMethodSignatureReturnType: false, useMethodSignatureParameterTypes: false, methodSignatureOpt: null, returnTypes: null, initialState: null, callbackOpt);
}
internal static void Analyze(
CSharpCompilation compilation,
BoundLambda lambda,
DiagnosticBag diagnostics,
MethodSymbol delegateInvokeMethod,
ArrayBuilder<(RefKind, TypeSymbolWithAnnotations)> returnTypes,
VariableState initialState)
{
Analyze(
compilation,
lambda.Symbol,
lambda.Body,
diagnostics,
useMethodSignatureReturnType: true,
useMethodSignatureParameterTypes: !lambda.UnboundLambda.HasExplicitlyTypedParameterList,
methodSignatureOpt: delegateInvokeMethod,
returnTypes, initialState,
callbackOpt: null);
}
private static void Analyze(
CSharpCompilation compilation,
MethodSymbol method,
BoundNode node,
DiagnosticBag diagnostics,
bool useMethodSignatureReturnType,
bool useMethodSignatureParameterTypes,
MethodSymbol methodSignatureOpt,
ArrayBuilder<(RefKind, TypeSymbolWithAnnotations)> returnTypes,
VariableState initialState,
Action callbackOpt)
{
Debug.Assert(diagnostics != null);
var walker = new NullableWalker(compilation, method, useMethodSignatureReturnType, useMethodSignatureParameterTypes, methodSignatureOpt, node, returnTypes, initialState, callbackOpt);
try
{
bool badRegion = false;
ImmutableArray returns = walker.Analyze(ref badRegion);
diagnostics.AddRange(walker.Diagnostics);
Debug.Assert(!badRegion);
}
catch (BoundTreeVisitor.CancelledByStackGuardException ex) when (diagnostics != null)
{
ex.AddAnError(diagnostics);
}
finally
{
walker.Free();
}
}
protected override void Normalize(ref LocalState state)
{
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++)
{
var value = GetDefaultState(ref state, slot);
state[slot] = value;
}
}
private bool? GetDefaultState(ref LocalState state, int slot)
{
if (slot == 0)
{
return null;
}
var variable = variableBySlot[slot];
var symbol = variable.Symbol;
switch (symbol.Kind)
{
case SymbolKind.Local:
return null;
case SymbolKind.Parameter:
{
var parameter = (ParameterSymbol)symbol;
if (parameter.RefKind == RefKind.Out)
{
return null;
}
TypeSymbolWithAnnotations parameterType;
if (!_variableTypes.TryGetValue(parameter, out parameterType))
{
parameterType = parameter.Type;
}
return !parameterType.IsNullable;
}
case SymbolKind.Field:
case SymbolKind.Property:
case SymbolKind.Event:
{
// PROTOTYPE(NullableReferenceTypes): State of containing struct should not be important.
// And if it is important, what about fields of structs that are fields of other structs, etc.?
int containingSlot = variable.ContainingSlot;
if (containingSlot > 0 &&
variableBySlot[containingSlot].Symbol.GetTypeOrReturnType().TypeKind == TypeKind.Struct &&
state[containingSlot] == null)
{
return null;
}
return !symbol.GetTypeOrReturnType().IsNullable;
}
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;
if (fieldSymbol.IsStatic || fieldSymbol.IsFixed)
{
return false;
}
member = fieldSymbol;
receiver = fieldAccess.ReceiverOpt;
break;
}
case BoundKind.EventAccess:
{
var eventAccess = (BoundEventAccess)expr;
var eventSymbol = eventAccess.EventSymbol;
if (eventSymbol.IsStatic)
{
return false;
}
// PROTOTYPE(NullableReferenceTypes): Use AssociatedField for field-like events?
member = eventSymbol;
receiver = eventAccess.ReceiverOpt;
break;
}
case BoundKind.PropertyAccess:
{
var propAccess = (BoundPropertyAccess)expr;
var propSymbol = propAccess.PropertySymbol;
if (propSymbol.IsStatic)
{
return false;
}
member = GetBackingFieldIfStructProperty(propSymbol);
receiver = propAccess.ReceiverOpt;
break;
}
}
return (object)member != null &&
(object)receiver != null &&
receiver.Kind != BoundKind.TypeExpression &&
(object)receiver.Type != null;
}
// PROTOTYPE(NullableReferenceTypes): Use backing field for struct property
// for now, to avoid cycles if the struct type contains a property of the struct type.
// Remove this and populate struct members lazily to match classes.
private Symbol GetBackingFieldIfStructProperty(Symbol symbol)
{
if (symbol.Kind == SymbolKind.Property)
{
var property = (PropertySymbol)symbol;
var containingType = property.ContainingType;
if (containingType.TypeKind == TypeKind.Struct)
{
// PROTOTYPE(NullableReferenceTypes): Relying on field name
// will not work for properties declared in other languages.
var fieldName = GeneratedNames.MakeBackingFieldName(property.Name);
return _emptyStructTypeCache.GetStructInstanceFields(containingType).FirstOrDefault(f => f.Name == fieldName);
}
}
return symbol;
}
// PROTOTYPE(NullableReferenceTypes): Temporary, until we're using
// properties on structs directly.
private new int GetOrCreateSlot(Symbol symbol, int containingSlot = 0)
{
symbol = GetBackingFieldIfStructProperty(symbol);
if ((object)symbol == null)
{
return -1;
}
return base.GetOrCreateSlot(symbol, containingSlot);
}
// PROTOTYPE(NullableReferenceTypes): Remove use of MakeSlot.
protected override int MakeSlot(BoundExpression node)
{
switch (node.Kind)
{
case BoundKind.ObjectCreationExpression:
case BoundKind.DynamicObjectCreationExpression:
case BoundKind.AnonymousObjectCreationExpression:
if (_placeholderLocals != null && _placeholderLocals.TryGetValue(node, out ObjectCreationPlaceholderLocal placeholder))
{
return GetOrCreateSlot(placeholder);
}
break;
case BoundKind.ConditionalReceiver:
if (_lastConditionalAccessSlot != -1)
{
int slot = _lastConditionalAccessSlot;
_lastConditionalAccessSlot = -1;
return slot;
}
break;
}
return base.MakeSlot(node);
}
private new void VisitLvalue(BoundExpression node)
{
switch (node.Kind)
{
case BoundKind.Local:
_resultType = GetDeclaredLocalResult(((BoundLocal)node).LocalSymbol);
break;
case BoundKind.Parameter:
_resultType = GetDeclaredParameterResult(((BoundParameter)node).ParameterSymbol);
break;
case BoundKind.FieldAccess:
{
var fieldAccess = (BoundFieldAccess)node;
VisitMemberAccess(fieldAccess.ReceiverOpt, fieldAccess.FieldSymbol, asLvalue: true);
}
break;
case BoundKind.PropertyAccess:
{
var propertyAccess = (BoundPropertyAccess)node;
VisitMemberAccess(propertyAccess.ReceiverOpt, propertyAccess.PropertySymbol, asLvalue: true);
}
break;
case BoundKind.EventAccess:
{
var eventAccess = (BoundEventAccess)node;
VisitMemberAccess(eventAccess.ReceiverOpt, eventAccess.EventSymbol, asLvalue: true);
}
break;
case BoundKind.ObjectInitializerMember:
throw ExceptionUtilities.UnexpectedValue(node.Kind); // Should have been handled in VisitObjectCreationExpression().
default:
VisitRvalue(node);
break;
}
if (_callbackOpt != null)
{
_callbackOpt(node, _resultType);
}
}
private TypeSymbolWithAnnotations VisitRvalueWithResult(BoundExpression node)
{
base.VisitRvalue(node);
return _resultType;
}
private static object GetTypeAsDiagnosticArgument(TypeSymbol typeOpt)
{
// PROTOTYPE(NullableReferenceTypes): Avoid hardcoded string.
return typeOpt ?? (object)"";
}
///
/// Reports top-level nullability problem in assignment.
///
private bool ReportNullReferenceAssignmentIfNecessary(BoundExpression value, TypeSymbolWithAnnotations targetType, TypeSymbolWithAnnotations valueType, bool useLegacyWarnings)
{
Debug.Assert(value != null);
Debug.Assert(!IsConditionalState);
if (targetType is null || valueType is null)
{
return false;
}
if (targetType.IsReferenceType && targetType.IsNullable == false && valueType.IsNullable == true)
{
if (useLegacyWarnings)
{
ReportWWarning(value.Syntax);
}
else if (!ReportNullAsNonNullableReferenceIfNecessary(value))
{
ReportStaticNullCheckingDiagnostics(ErrorCode.WRN_NullReferenceAssignment, value.Syntax);
}
return true;
}
return false;
}
private void ReportAssignmentWarnings(BoundExpression value, TypeSymbolWithAnnotations targetType, TypeSymbolWithAnnotations valueType, bool useLegacyWarnings)
{
Debug.Assert(value != null);
Debug.Assert(!IsConditionalState);
if (this.State.Reachable)
{
if (targetType is null || valueType is null)
{
return;
}
ReportNullReferenceAssignmentIfNecessary(value, targetType, valueType, useLegacyWarnings);
ReportNullabilityMismatchInAssignmentIfNecessary(value, valueType.TypeSymbol, targetType.TypeSymbol);
}
}
///
/// Update tracked value on assignment.
///
private void TrackNullableStateForAssignment(BoundExpression value, TypeSymbolWithAnnotations targetType, int targetSlot, TypeSymbolWithAnnotations valueType, int valueSlot = -1)
{
Debug.Assert(value != null);
Debug.Assert(!IsConditionalState);
if (this.State.Reachable)
{
if ((object)targetType == null)
{
return;
}
if (targetSlot <= 0)
{
return;
}
bool isByRefTarget = IsByRefTarget(targetSlot);
if (targetSlot >= this.State.Capacity) Normalize(ref this.State);
this.State[targetSlot] = isByRefTarget ?
// Since reference can point to the heap, we cannot assume the value is not null after this assignment,
// regardless of what value is being assigned.
(targetType.IsNullable == true) ? (bool?)false : null :
!valueType?.IsNullable;
// PROTOTYPE(NullableReferenceTypes): Might this clear state that
// should be copied in InheritNullableStateOfTrackableType?
InheritDefaultState(targetSlot);
if (targetType.IsReferenceType)
{
// PROTOTYPE(NullableReferenceTypes): We should copy all tracked state from `value`,
// regardless of BoundNode type, but we'll need to handle cycles. (For instance, the
// assignment to C.F below. See also StaticNullChecking_Members.FieldCycle_01.)
// class C
// {
// C? F;
// C() { F = this; }
// }
// For now, we copy a limited set of BoundNode types that shouldn't contain cycles.
if ((value.Kind == BoundKind.ObjectCreationExpression || value.Kind == BoundKind.AnonymousObjectCreationExpression || value.Kind == BoundKind.DynamicObjectCreationExpression || targetType.TypeSymbol.IsAnonymousType) &&
targetType.TypeSymbol.Equals(valueType?.TypeSymbol, TypeCompareKind.ConsiderEverything)) // PROTOTYPE(NullableReferenceTypes): Allow assignment to base type.
{
if (valueSlot > 0)
{
InheritNullableStateOfTrackableType(targetSlot, valueSlot, isByRefTarget, slotWatermark: GetSlotWatermark());
}
}
}
else if (EmptyStructTypeCache.IsTrackableStructType(targetType.TypeSymbol) &&
targetType.TypeSymbol.Equals(valueType?.TypeSymbol, TypeCompareKind.ConsiderEverything))
{
InheritNullableStateOfTrackableStruct(targetType.TypeSymbol, targetSlot, valueSlot, IsByRefTarget(targetSlot), slotWatermark: GetSlotWatermark());
}
}
}
private int GetSlotWatermark() => this.nextVariableSlot;
private bool IsByRefTarget(int slot)
{
if (slot > 0)
{
Symbol associatedNonMemberSymbol = GetNonMemberSymbol(slot);
switch (associatedNonMemberSymbol.Kind)
{
case SymbolKind.Local:
return ((LocalSymbol)associatedNonMemberSymbol).RefKind != RefKind.None;
case SymbolKind.Parameter:
var parameter = (ParameterSymbol)associatedNonMemberSymbol;
return !parameter.IsThis && parameter.RefKind != RefKind.None;
}
}
return false;
}
private void ReportWWarning(SyntaxNode syntax)
{
ReportStaticNullCheckingDiagnostics(ErrorCode.WRN_ConvertingNullableToNonNullable, syntax);
}
private void ReportStaticNullCheckingDiagnostics(ErrorCode errorCode, SyntaxNode syntaxNode, params object[] arguments)
{
if (!_disableDiagnostics)
{
Diagnostics.Add(errorCode, syntaxNode.GetLocation(), arguments);
}
}
private void InheritNullableStateOfTrackableStruct(TypeSymbol targetType, int targetSlot, int valueSlot, bool isByRefTarget, int slotWatermark)
{
Debug.Assert(targetSlot > 0);
Debug.Assert(EmptyStructTypeCache.IsTrackableStructType(targetType));
// PROTOTYPE(NullableReferenceTypes): Handle properties not backed by fields.
// See ModifyMembers_StructPropertyNoBackingField and PropertyCycle_Struct tests.
foreach (var field in _emptyStructTypeCache.GetStructInstanceFields(targetType))
{
InheritNullableStateOfMember(targetSlot, valueSlot, field, isByRefTarget, slotWatermark);
}
}
// 'slotWatermark' is used to avoid inheriting members from inherited members.
private void InheritNullableStateOfMember(int targetContainerSlot, int valueContainerSlot, Symbol member, bool isByRefTarget, int slotWatermark)
{
Debug.Assert(valueContainerSlot <= slotWatermark);
TypeSymbolWithAnnotations fieldOrPropertyType = member.GetTypeOrReturnType();
if (fieldOrPropertyType.IsReferenceType)
{
// If statically declared as not-nullable, no need to adjust the tracking info.
// Declaration information takes priority.
if (fieldOrPropertyType.IsNullable != false)
{
int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot);
bool? value = !fieldOrPropertyType.IsNullable;
if (isByRefTarget)
{
// This is a member access through a by ref entity and it isn't considered declared as not-nullable.
// Since reference can point to the heap, we cannot assume the member doesn't have null value
// after this assignment, regardless of what value is being assigned.
}
else if (valueContainerSlot > 0)
{
int valueMemberSlot = VariableSlot(member, valueContainerSlot);
value = valueMemberSlot > 0 && valueMemberSlot < this.State.Capacity ?
this.State[valueMemberSlot] :
null;
}
this.State[targetMemberSlot] = value;
}
if (valueContainerSlot > 0)
{
int valueMemberSlot = VariableSlot(member, valueContainerSlot);
if (valueMemberSlot > 0 && valueMemberSlot <= slotWatermark)
{
int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot);
InheritNullableStateOfTrackableType(targetMemberSlot, valueMemberSlot, isByRefTarget, slotWatermark);
}
}
}
else if (EmptyStructTypeCache.IsTrackableStructType(fieldOrPropertyType.TypeSymbol))
{
int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot);
if (targetMemberSlot > 0)
{
int valueMemberSlot = -1;
if (valueContainerSlot > 0)
{
int slot = GetOrCreateSlot(member, valueContainerSlot);
if (slot < slotWatermark)
{
valueMemberSlot = slot;
}
}
InheritNullableStateOfTrackableStruct(fieldOrPropertyType.TypeSymbol, targetMemberSlot, valueMemberSlot, isByRefTarget, slotWatermark);
}
}
}
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;
}
this.State[slot] = !variable.Symbol.GetTypeOrReturnType().IsNullable;
InheritDefaultState(slot);
}
}
private void InheritNullableStateOfTrackableType(int targetSlot, int valueSlot, bool isByRefTarget, int slotWatermark)
{
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, isByRefTarget, slotWatermark);
}
}
protected override LocalState ReachableState()
{
var state = new LocalState(BitVector.Create(nextVariableSlot), BitVector.Create(nextVariableSlot));
Populate(ref state, start: 0);
return state;
}
protected override LocalState UnreachableState()
{
return new LocalState(BitVector.Empty, BitVector.Empty);
}
protected override LocalState AllBitsSet()
{
return new LocalState(BitVector.Create(nextVariableSlot), BitVector.Create(nextVariableSlot));
}
private void EnterParameters()
{
var methodParameters = ((MethodSymbol)_member).Parameters;
var signatureParameters = _useMethodSignatureParameterTypes ? _methodSignatureOpt.Parameters : methodParameters;
Debug.Assert(signatureParameters.Length == methodParameters.Length);
int n = methodParameters.Length;
for (int i = 0; i < n; i++)
{
var parameter = methodParameters[i];
var parameterType = signatureParameters[i].Type;
_variableTypes[parameter] = parameterType;
EnterParameter(parameter, parameterType);
}
}
private void EnterParameter(ParameterSymbol parameter, TypeSymbolWithAnnotations parameterType)
{
int slot = GetOrCreateSlot(parameter);
Debug.Assert(!IsConditionalState);
if (slot > 0 && parameter.RefKind != RefKind.Out)
{
if (EmptyStructTypeCache.IsTrackableStructType(parameterType.TypeSymbol))
{
InheritNullableStateOfTrackableStruct(parameterType.TypeSymbol, slot, valueSlot: -1, isByRefTarget: parameter.RefKind != RefKind.None, slotWatermark: GetSlotWatermark());
}
}
}
#region Visitors
public override BoundNode VisitIsPatternExpression(BoundIsPatternExpression node)
{
// PROTOTYPE(NullableReferenceTypes): Move these asserts to base class.
Debug.Assert(!IsConditionalState);
// Create slot when the state is unconditional since EnsureCapacity should be
// called on all fields and that is simpler if state is limited to this.State.
int slot = -1;
if (this.State.Reachable)
{
var pattern = node.Pattern;
// PROTOTYPE(NullableReferenceTypes): Handle patterns that ensure x is not null:
// x is T y // where T is not inferred via var
// x is K // where K is a constant other than null
if (pattern.Kind == BoundKind.ConstantPattern && ((BoundConstantPattern)pattern).ConstantValue?.IsNull == true)
{
slot = MakeSlot(node.Expression);
if (slot > 0)
{
Normalize(ref this.State);
}
}
}
var result = base.VisitIsPatternExpression(node);
Debug.Assert(IsConditionalState);
if (slot > 0)
{
this.StateWhenTrue[slot] = false;
this.StateWhenFalse[slot] = true;
}
SetResult(node);
return result;
}
public override void VisitPattern(BoundExpression expression, BoundPattern pattern)
{
base.VisitPattern(expression, pattern);
var whenFail = StateWhenFalse;
SetState(StateWhenTrue);
AssignPatternVariables(pattern);
SetConditionalState(this.State, whenFail);
SetUnknownResultNullability();
}
private void AssignPatternVariables(BoundPattern pattern)
{
switch (pattern.Kind)
{
case BoundKind.DeclarationPattern:
// PROTOTYPE(NullableReferenceTypes): Handle.
break;
case BoundKind.WildcardPattern:
break;
case BoundKind.ConstantPattern:
{
var pat = (BoundConstantPattern)pattern;
this.VisitRvalue(pat.Value);
break;
}
default:
break;
}
}
protected override BoundNode VisitReturnStatementNoAdjust(BoundReturnStatement node)
{
Debug.Assert(!IsConditionalState);
BoundExpression expr = node.ExpressionOpt;
if (expr == null)
{
return null;
}
Conversion conversion;
(expr, conversion) = RemoveConversion(expr, includeExplicitConversions: false);
TypeSymbolWithAnnotations result = VisitRvalueWithResult(expr);
//if (this.State.Reachable) // PROTOTYPE(NullableReferenceTypes): Consider reachability?
if (expr.Type?.IsErrorType() == true)
{
return null;
}
if (_returnTypes != null)
{
// Inferring return type. Should not convert to method return type.
_returnTypes.Add((node.RefKind, result));
return null;
}
// Convert to method return type.
var returnType = GetReturnType();
TypeSymbolWithAnnotations resultType = ApplyConversion(expr, expr, conversion, returnType.TypeSymbol, result, checkConversion: true, fromExplicitCast: false, out bool canConvertNestedNullability);
if (!canConvertNestedNullability)
{
ReportStaticNullCheckingDiagnostics(ErrorCode.WRN_NullabilityMismatchInAssignment, expr.Syntax, GetTypeAsDiagnosticArgument(result?.TypeSymbol), returnType.TypeSymbol);
}
bool returnTypeIsNonNullable = IsNonNullable(returnType);
bool returnTypeIsUnconstrainedTypeParameter = IsUnconstrainedTypeParameter(returnType.TypeSymbol);
bool reportedNullable = false;
if (returnTypeIsNonNullable || returnTypeIsUnconstrainedTypeParameter)
{
reportedNullable = ReportNullAsNonNullableReferenceIfNecessary(node.ExpressionOpt);
}
if (!reportedNullable)
{
if (IsNullable(resultType) && (returnTypeIsNonNullable || returnTypeIsUnconstrainedTypeParameter) ||
IsUnconstrainedTypeParameter(resultType?.TypeSymbol) && returnTypeIsNonNullable)
{
ReportStaticNullCheckingDiagnostics(ErrorCode.WRN_NullReferenceReturn, node.ExpressionOpt.Syntax);
}
}
return null;
}
private TypeSymbolWithAnnotations GetReturnType()
{
var method = (MethodSymbol)_member;
var returnType = (_useMethodSignatureReturnType ? _methodSignatureOpt : method).ReturnType;
Debug.Assert((object)returnType != LambdaSymbol.ReturnTypeIsBeingInferred);
return method.IsGenericTaskReturningAsync(compilation) ?
((NamedTypeSymbol)returnType.TypeSymbol).TypeArgumentsNoUseSiteDiagnostics.Single() :
returnType;
}
private static bool IsNullable(TypeSymbolWithAnnotations typeOpt)
{
return typeOpt?.IsNullable == true;
}
private static bool IsNonNullable(TypeSymbolWithAnnotations typeOpt)
{
return typeOpt?.IsNullable == false && typeOpt.IsReferenceType;
}
private static bool IsUnconstrainedTypeParameter(TypeSymbol typeOpt)
{
return typeOpt?.IsUnconstrainedTypeParameter() == true;
}
///
/// Report warning assigning value where nested nullability does not match
/// target (e.g.: `object[] a = new[] { maybeNull }`).
///
private void ReportNullabilityMismatchInAssignmentIfNecessary(BoundExpression node, TypeSymbol sourceType, TypeSymbol destinationType)
{
if ((object)sourceType != null && IsNullabilityMismatch(destinationType, sourceType))
{
ReportStaticNullCheckingDiagnostics(ErrorCode.WRN_NullabilityMismatchInAssignment, node.Syntax, sourceType, destinationType);
}
}
public override BoundNode VisitLocal(BoundLocal node)
{
var local = node.LocalSymbol;
int slot = GetOrCreateSlot(local);
var type = GetDeclaredLocalResult(local);
_resultType = GetAdjustedResult(type, slot);
return null;
}
public override BoundNode VisitLocalDeclaration(BoundLocalDeclaration node)
{
var local = node.LocalSymbol;
int slot = GetOrCreateSlot(local);
var initializer = node.InitializerOpt;
if (initializer is null)
{
return null;
}
Conversion conversion;
(initializer, conversion) = RemoveConversion(initializer, includeExplicitConversions: false);
TypeSymbolWithAnnotations valueType = VisitRvalueWithResult(initializer);
TypeSymbolWithAnnotations type = local.Type;
if (node.DeclaredType.InferredType)
{
Debug.Assert(conversion.IsIdentity);
if (valueType is null)
{
Debug.Assert(type.IsErrorType());
valueType = type;
}
_variableTypes[local] = valueType;
type = valueType;
}
else
{
var unconvertedType = valueType;
valueType = ApplyConversion(initializer, initializer, conversion, type.TypeSymbol, valueType, checkConversion: true, fromExplicitCast: false, out bool canConvertNestedNullability);
// Need to report all warnings that apply since the warnings can be suppressed individually.
ReportNullReferenceAssignmentIfNecessary(initializer, type, valueType, useLegacyWarnings: true);
if (!canConvertNestedNullability)
{
ReportStaticNullCheckingDiagnostics(ErrorCode.WRN_NullabilityMismatchInAssignment, initializer.Syntax, GetTypeAsDiagnosticArgument(unconvertedType?.TypeSymbol), type.TypeSymbol);
}
}
TrackNullableStateForAssignment(initializer, type, slot, valueType, MakeSlot(initializer));
return null;
}
protected override BoundExpression VisitExpressionWithoutStackGuard(BoundExpression node)
{
Debug.Assert(!IsConditionalState);
_resultType = _invalidType; // PROTOTYPE(NullableReferenceTypes): Move to `Visit` method?
var result = base.VisitExpressionWithoutStackGuard(node);
#if DEBUG
// Verify Visit method set _result.
TypeSymbolWithAnnotations resultType = _resultType;
Debug.Assert((object)resultType != _invalidType);
Debug.Assert(AreCloseEnough(resultType?.TypeSymbol, node.Type));
#endif
if (_callbackOpt != null)
{
_callbackOpt(node, _resultType);
}
return result;
}
#if DEBUG
// For asserts only.
private static bool AreCloseEnough(TypeSymbol typeA, TypeSymbol typeB)
{
if ((object)typeA == typeB)
{
return true;
}
if (typeA is null || typeB is null)
{
return false;
}
bool canIgnoreType(TypeSymbol type) => (object)type.VisitType((t, unused1, unused2) => t.IsErrorType() || t.IsDynamic() || t.HasUseSiteError, (object)null) != null;
return canIgnoreType(typeA) ||
canIgnoreType(typeB) ||
typeA.Equals(typeB, TypeCompareKind.IgnoreCustomModifiersAndArraySizesAndLowerBounds | TypeCompareKind.IgnoreDynamicAndTupleNames); // Ignore TupleElementNames (see https://github.com/dotnet/roslyn/issues/23651).
}
#endif
protected override void VisitStatement(BoundStatement statement)
{
_resultType = _invalidType;
base.VisitStatement(statement);
_resultType = _invalidType;
}
public override BoundNode VisitObjectCreationExpression(BoundObjectCreationExpression node)
{
Debug.Assert(!IsConditionalState);
VisitArguments(node, node.Arguments, node.ArgumentRefKindsOpt, node.Constructor, node.ArgsToParamsOpt, node.Expanded);
VisitObjectOrDynamicObjectCreation(node, node.InitializerExpressionOpt);
return null;
}
private void VisitObjectOrDynamicObjectCreation(BoundExpression node, BoundExpression initializerOpt)
{
Debug.Assert(node.Kind == BoundKind.ObjectCreationExpression || node.Kind == BoundKind.DynamicObjectCreationExpression);
LocalSymbol receiver = null;
int slot = -1;
TypeSymbol type = node.Type;
if ((object)type != null)
{
bool isTrackableStructType = EmptyStructTypeCache.IsTrackableStructType(type);
if (type.IsReferenceType || isTrackableStructType)
{
receiver = GetOrCreateObjectCreationPlaceholder(node);
slot = GetOrCreateSlot(receiver);
if (slot > 0 && isTrackableStructType)
{
this.State[slot] = true;
InheritNullableStateOfTrackableStruct(type, slot, valueSlot: -1, isByRefTarget: false, slotWatermark: GetSlotWatermark());
}
}
}
if (initializerOpt != null)
{
VisitObjectCreationInitializer(receiver, slot, initializerOpt);
}
_resultType = TypeSymbolWithAnnotations.Create(type);
}
private void VisitObjectCreationInitializer(Symbol containingSymbol, int containingSlot, BoundExpression node)
{
switch (node.Kind)
{
case BoundKind.ObjectInitializerExpression:
foreach (var initializer in ((BoundObjectInitializerExpression)node).Initializers)
{
switch (initializer.Kind)
{
case BoundKind.AssignmentOperator:
VisitObjectElementInitializer(containingSymbol, containingSlot, (BoundAssignmentOperator)initializer);
break;
default:
VisitRvalue(initializer);
break;
}
}
break;
case BoundKind.CollectionInitializerExpression:
foreach (var initializer in ((BoundCollectionInitializerExpression)node).Initializers)
{
switch (initializer.Kind)
{
case BoundKind.CollectionElementInitializer:
VisitCollectionElementInitializer((BoundCollectionElementInitializer)initializer);
break;
default:
VisitRvalue(initializer);
break;
}
}
break;
default:
TypeSymbolWithAnnotations resultType = VisitRvalueWithResult(node);
if ((object)containingSymbol != null)
{
var type = containingSymbol.GetTypeOrReturnType();
ReportAssignmentWarnings(node, type, resultType, useLegacyWarnings: false);
TrackNullableStateForAssignment(node, type, containingSlot, resultType, MakeSlot(node));
}
break;
}
}
private void VisitObjectElementInitializer(Symbol containingSymbol, 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);
}
}
break;
default:
VisitLvalue(node);
break;
}
}
private new void VisitCollectionElementInitializer(BoundCollectionElementInitializer node)
{
if (node.AddMethod.CallsAreOmitted(node.SyntaxTree))
{
// PROTOTYPE(NullableReferenceTypes): Should skip state set in arguments
// of omitted call. See PreciseAbstractFlowPass.VisitCollectionElementInitializer.
}
VisitArguments(node, node.Arguments, default(ImmutableArray), node.AddMethod, node.ArgsToParamsOpt, node.Expanded);
SetUnknownResultNullability();
}
private void SetResult(BoundExpression node)
{
_resultType = TypeSymbolWithAnnotations.Create(node.Type);
}
private ObjectCreationPlaceholderLocal GetOrCreateObjectCreationPlaceholder(BoundExpression node)
{
ObjectCreationPlaceholderLocal placeholder;
if (_placeholderLocals == null)
{
_placeholderLocals = PooledDictionary.GetInstance();
placeholder = null;
}
else
{
_placeholderLocals.TryGetValue(node, out placeholder);
}
if ((object)placeholder == null)
{
placeholder = new ObjectCreationPlaceholderLocal(_member, node);
_placeholderLocals.Add(node, placeholder);
}
return placeholder;
}
public override BoundNode VisitAnonymousObjectCreationExpression(BoundAnonymousObjectCreationExpression node)
{
Debug.Assert(!IsConditionalState);
int receiverSlot = -1;
var arguments = node.Arguments;
var constructor = node.Constructor;
for (int i = 0; i < arguments.Length; i++)
{
var argument = arguments[i];
TypeSymbolWithAnnotations argumentType = VisitRvalueWithResult(argument);
var parameter = constructor.Parameters[i];
ReportArgumentWarnings(argument, argumentType, parameter);
// PROTOTYPE(NullableReferenceTypes): node.Declarations includes
// explicitly-named properties only. For now, skip expressions
// with implicit names. See NullableReferenceTypesTests.AnonymousTypes_05.
if (node.Declarations.Length < arguments.Length)
{
continue;
}
PropertySymbol property = node.Declarations[i].Property;
if (receiverSlot <= 0)
{
ObjectCreationPlaceholderLocal implicitReceiver = GetOrCreateObjectCreationPlaceholder(node);
receiverSlot = GetOrCreateSlot(implicitReceiver);
}
ReportAssignmentWarnings(argument, property.Type, argumentType, useLegacyWarnings: false);
TrackNullableStateForAssignment(argument, property.Type, GetOrCreateSlot(property, receiverSlot), argumentType, MakeSlot(argument));
}
// PROTOTYPE(NullableReferenceTypes): _result may need to be a new anonymous
// type since the properties may have distinct nullability from original.
// (See StaticNullChecking_FlowAnalysis.AnonymousObjectCreation_02.)
_resultType = TypeSymbolWithAnnotations.Create(node.Type);
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);
_resultType = TypeSymbolWithAnnotations.Create(resultType);
return null;
}
private ArrayTypeSymbol VisitArrayInitializer(BoundArrayCreation node)
{
var arrayType = (ArrayTypeSymbol)node.Type;
var elementType = arrayType.ElementType;
BoundArrayInitialization initialization = node.InitializerOpt;
var elementBuilder = ArrayBuilder.GetInstance(initialization.Initializers.Length);
GetArrayElements(initialization, elementBuilder);
// PROTOTYPE(NullableReferenceTypes): Removing and recalculating conversions should not
// be necessary for explicitly typed arrays. In those cases, VisitConversion should warn
// on nullability mismatch (although we'll need to ensure we handle the case where
// initial binding calculated an Identity conversion, even though nullability was distinct).
int n = elementBuilder.Count;
var conversionBuilder = ArrayBuilder.GetInstance(n);
var resultBuilder = ArrayBuilder.GetInstance(n);
for (int i = 0; i < n; i++)
{
(BoundExpression element, Conversion conversion) = RemoveConversion(elementBuilder[i], includeExplicitConversions: false);
elementBuilder[i] = element;
conversionBuilder.Add(conversion);
resultBuilder.Add(VisitRvalueWithResult(element));
}
// PROTOTYPE(NullableReferenceTypes): Record in the BoundArrayCreation
// whether the array was implicitly typed, rather than relying on syntax.
if (node.Syntax.Kind() == SyntaxKind.ImplicitArrayCreationExpression)
{
var resultTypes = resultBuilder.ToImmutable();
// PROTOTYPE(NullableReferenceTypes): Initial binding calls InferBestType(ImmutableArray, ...)
// overload. Why are we calling InferBestType(ImmutableArray, ...) here?
// PROTOTYPE(NullableReferenceTypes): InferBestType(ImmutableArray, ...)
// uses a HashSet to reduce the candidates to the unique types before comparing.
// Should do the same here.
HashSet useSiteDiagnostics = null;
// If there are error types, use the first error type. (Matches InferBestType(ImmutableArray, ...).)
var bestType = resultTypes.FirstOrDefault(t => t?.IsErrorType() == true) ??
BestTypeInferrer.InferBestType(resultTypes, _conversions, useSiteDiagnostics: ref useSiteDiagnostics);
// PROTOTYPE(NullableReferenceTypes): Report a special ErrorCode.WRN_NoBestNullabilityArrayElements
// when InferBestType fails, and avoid reporting conversion warnings for each element in those cases.
// (See similar code for conditional expressions: ErrorCode.WRN_NoBestNullabilityConditionalExpression.)
if ((object)bestType != null)
{
elementType = bestType;
}
arrayType = arrayType.WithElementType(elementType);
}
if ((object)elementType != null)
{
bool elementTypeIsReferenceType = elementType.IsReferenceType == true;
for (int i = 0; i < n; i++)
{
var conversion = conversionBuilder[i];
var element = elementBuilder[i];
var resultType = resultBuilder[i];
var sourceType = resultType?.TypeSymbol;
if (elementTypeIsReferenceType)
{
resultType = ApplyConversion(element, element, conversion, elementType.TypeSymbol, resultType, checkConversion: true, fromExplicitCast: false, out bool canConvertNestedNullability);
ReportNullReferenceAssignmentIfNecessary(element, elementType, resultType, useLegacyWarnings: false);
if (!canConvertNestedNullability)
{
ReportStaticNullCheckingDiagnostics(ErrorCode.WRN_NullabilityMismatchInAssignment, element.Syntax, sourceType, elementType.TypeSymbol);
}
}
}
}
resultBuilder.Free();
elementBuilder.Free();
_resultType = _invalidType;
return arrayType;
}
private static void GetArrayElements(BoundArrayInitialization node, ArrayBuilder 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);
VisitRvalue(node.Expression);
Debug.Assert(!IsConditionalState);
// No need to check expression type since System.Array is a reference type.
Debug.Assert(node.Expression.Type.IsReferenceType);
CheckPossibleNullReceiver(node.Expression, checkType: false);
var type = _resultType?.TypeSymbol as ArrayTypeSymbol;
foreach (var i in node.Indices)
{
VisitRvalue(i);
}
_resultType = type?.ElementType;
return null;
}
private TypeSymbolWithAnnotations InferResultNullability(BoundBinaryOperator node, TypeSymbolWithAnnotations leftType, TypeSymbolWithAnnotations rightType)
{
return InferResultNullability(node.OperatorKind, node.MethodOpt, node.Type, leftType, rightType);
}
private TypeSymbolWithAnnotations InferResultNullability(BinaryOperatorKind operatorKind, MethodSymbol methodOpt, TypeSymbol resultType, TypeSymbolWithAnnotations leftType, TypeSymbolWithAnnotations rightType)
{
bool? isNullable = null;
if (operatorKind.IsUserDefined())
{
if (operatorKind.IsLifted())
{
// PROTOTYPE(NullableReferenceTypes): Conversions: Lifted operator
return TypeSymbolWithAnnotations.Create(resultType, isNullableIfReferenceType: null);
}
// PROTOTYPE(NullableReferenceTypes): Update method based on operand types.
if ((object)methodOpt != null && methodOpt.ParameterCount == 2)
{
return methodOpt.ReturnType;
}
}
else if (!operatorKind.IsDynamic() && resultType.IsReferenceType == true)
{
switch (operatorKind.Operator() | operatorKind.OperandTypes())
{
case BinaryOperatorKind.DelegateCombination:
{
bool? leftIsNullable = leftType?.IsNullable;
bool? rightIsNullable = rightType?.IsNullable;
if (leftIsNullable == false || rightIsNullable == false)
{
isNullable = false;
}
else if (leftIsNullable == true && rightIsNullable == true)
{
isNullable = true;
}
else
{
Debug.Assert(leftIsNullable == null || rightIsNullable == null);
}
}
break;
case BinaryOperatorKind.DelegateRemoval:
isNullable = true; // Delegate removal can produce null.
break;
default:
isNullable = false;
break;
}
}
return TypeSymbolWithAnnotations.Create(resultType, isNullable);
}
protected override void AfterLeftChildHasBeenVisited(BoundBinaryOperator binary)
{
Debug.Assert(!IsConditionalState);
//if (this.State.Reachable) // PROTOTYPE(NullableReferenceTypes): Consider reachability?
{
TypeSymbolWithAnnotations leftType = _resultType;
bool warnOnNullReferenceArgument = (binary.OperatorKind.IsUserDefined() && (object)binary.MethodOpt != null && binary.MethodOpt.ParameterCount == 2);
if (warnOnNullReferenceArgument)
{
ReportArgumentWarnings(binary.Left, leftType, binary.MethodOpt.Parameters[0]);
}
VisitRvalue(binary.Right);
Debug.Assert(!IsConditionalState);
// At this point, State.Reachable may be false for
// invalid code such as `s + throw new Exception()`.
TypeSymbolWithAnnotations rightType = _resultType;
if (warnOnNullReferenceArgument)
{
ReportArgumentWarnings(binary.Right, rightType, binary.MethodOpt.Parameters[1]);
}
Debug.Assert(!IsConditionalState);
_resultType = InferResultNullability(binary, leftType, rightType);
BinaryOperatorKind op = binary.OperatorKind.Operator();
if (op == BinaryOperatorKind.Equal || op == BinaryOperatorKind.NotEqual)
{
BoundExpression operandComparedToNull = null;
TypeSymbolWithAnnotations operandComparedToNullType = null;
if (binary.Right.ConstantValue?.IsNull == true)
{
operandComparedToNull = binary.Left;
operandComparedToNullType = leftType;
}
else if (binary.Left.ConstantValue?.IsNull == true)
{
operandComparedToNull = binary.Right;
operandComparedToNullType = rightType;
}
if (operandComparedToNull != null)
{
// PROTOTYPE(NullableReferenceTypes): This check is incorrect since it compares declared
// nullability rather than tracked nullability. Moreover, we should only report such
// diagnostics for locals that are set or checked explicitly within this method.
if (operandComparedToNullType?.IsNullable == false)
{
ReportStaticNullCheckingDiagnostics(op == BinaryOperatorKind.Equal ?
ErrorCode.HDN_NullCheckIsProbablyAlwaysFalse :
ErrorCode.HDN_NullCheckIsProbablyAlwaysTrue,
binary.Syntax);
}
// Skip reference conversions
operandComparedToNull = SkipReferenceConversions(operandComparedToNull);
if (operandComparedToNull.Type?.IsReferenceType == true)
{
var slotBuilder = ArrayBuilder.GetInstance();
// Set all nested conditional slots. For example in a?.b?.c we'll set a, b, and c
getOperandSlots(operandComparedToNull, slotBuilder);
if (slotBuilder.Count != 0)
{
Normalize(ref this.State);
Split();
ref LocalState state = ref (op == BinaryOperatorKind.Equal) ? ref this.StateWhenFalse : ref this.StateWhenTrue;
foreach (int slot in slotBuilder)
{
state[slot] = true;
}
}
slotBuilder.Free();
}
}
}
}
void getOperandSlots(BoundExpression operand, ArrayBuilder slotBuilder)
{
Debug.Assert(operand != null);
Debug.Assert(_lastConditionalAccessSlot == -1);
do
{
// 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
// locals 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:
// PROTOTYPE(NullableReferenceTypes): 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)
{
// If we got a slot we must have processed the previous conditional receiver.
Debug.Assert(_lastConditionalAccessSlot == -1);
slotBuilder.Add(slot);
// 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.
// PROTOTYPE(NullableReferenceTypes): 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)
{
// If we got a slot then all previous BoundCondtionalReceivers must have been handled.
Debug.Assert(_lastConditionalAccessSlot == -1);
slotBuilder.Add(slot);
}
break;
}
// If we didn't get a slot, it's possible that the current _lastConditionalSlot was never processed,
// so we reset before leaving the function.
_lastConditionalAccessSlot = -1;
return;
} while (true);
}
}
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 VisitNullCoalescingOperator(BoundNullCoalescingOperator node)
{
Debug.Assert(!IsConditionalState);
BoundExpression leftOperand = node.LeftOperand;
BoundExpression rightOperand = node.RightOperand;
TypeSymbolWithAnnotations leftResult = VisitRvalueWithResult(leftOperand);
TypeSymbolWithAnnotations rightResult;
if (IsConstantNull(leftOperand))
{
rightResult = VisitRvalueWithResult(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.
_resultType = TypeSymbolWithAnnotations.Create(node.Type, getIsNullable(rightOperand, rightResult));
return null;
}
var leftState = this.State.Clone();
if (leftResult?.IsNullable == false)
{
ReportStaticNullCheckingDiagnostics(ErrorCode.HDN_ExpressionIsProbablyNeverNull, leftOperand.Syntax);
}
bool leftIsConstant = leftOperand.ConstantValue != null;
if (leftIsConstant)
{
SetUnreachable();
}
// PROTOTYPE(NullableReferenceTypes): For cases where the left operand determines
// the type, we should unwrap the right conversion and re-apply.
rightResult = VisitRvalueWithResult(rightOperand);
IntersectWith(ref this.State, ref leftState);
TypeSymbol resultType;
var leftResultType = leftResult?.TypeSymbol;
var rightResultType = rightResult?.TypeSymbol;
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);
}
bool? resultIsNullable = getIsNullable(leftOperand, leftResult) == false ? false : getIsNullable(rightOperand, rightResult);
_resultType = TypeSymbolWithAnnotations.Create(resultType, resultIsNullable);
return null;
bool? getIsNullable(BoundExpression e, TypeSymbolWithAnnotations t) => (t is null) ? e.IsNullable() : t.IsNullable;
TypeSymbol getLeftResultType(TypeSymbol leftType, TypeSymbol rightType)
{
// 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 ((object)rightType != null &&
(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;
}
}
public override BoundNode VisitConditionalAccess(BoundConditionalAccess node)
{
Debug.Assert(!IsConditionalState);
var receiver = node.Receiver;
var receiverType = VisitRvalueWithResult(receiver);
var receiverState = this.State.Clone();
if (receiver.Type?.IsReferenceType == true)
{
if (receiverType?.IsNullable == false)
{
ReportStaticNullCheckingDiagnostics(ErrorCode.HDN_ExpressionIsProbablyNeverNull, receiver.Syntax);
}
int slot = MakeSlot(SkipReferenceConversions(receiver));
if (slot > 0)
{
if (slot >= this.State.Capacity) Normalize(ref this.State);
this.State[slot] = true;
}
}
if (IsConstantNull(node.Receiver))
{
SetUnreachable();
}
VisitRvalue(node.AccessExpression);
IntersectWith(ref this.State, ref receiverState);
// PROTOTYPE(NullableReferenceTypes): Use flow analysis type rather than node.Type
// so that nested nullability is inferred from flow analysis. See VisitConditionalOperator.
_resultType = TypeSymbolWithAnnotations.Create(node.Type, isNullableIfReferenceType: receiverType?.IsNullable | _resultType?.IsNullable);
// PROTOTYPE(NullableReferenceTypes): Report conversion warnings.
return null;
}
public override BoundNode VisitConditionalOperator(BoundConditionalOperator node)
{
var isByRef = node.IsRef;
VisitCondition(node.Condition);
var consequenceState = this.StateWhenTrue;
var alternativeState = this.StateWhenFalse;
BoundExpression consequence;
BoundExpression alternative;
TypeSymbolWithAnnotations consequenceResult;
TypeSymbolWithAnnotations alternativeResult;
bool? isNullableIfReferenceType;
if (IsConstantTrue(node.Condition))
{
(alternative, alternativeResult) = visitConditionalOperand(alternativeState, node.Alternative);
(consequence, consequenceResult) = visitConditionalOperand(consequenceState, node.Consequence);
isNullableIfReferenceType = getIsNullableIfReferenceType(consequence, consequenceResult);
}
else if (IsConstantFalse(node.Condition))
{
(consequence, consequenceResult) = visitConditionalOperand(consequenceState, node.Consequence);
(alternative, alternativeResult) = visitConditionalOperand(alternativeState, node.Alternative);
isNullableIfReferenceType = getIsNullableIfReferenceType(alternative, alternativeResult);
}
else
{
(consequence, consequenceResult) = visitConditionalOperand(consequenceState, node.Consequence);
Unsplit();
(alternative, alternativeResult) = visitConditionalOperand(alternativeState, node.Alternative);
Unsplit();
IntersectWith(ref this.State, ref consequenceState);
isNullableIfReferenceType = (getIsNullableIfReferenceType(consequence, consequenceResult) | getIsNullableIfReferenceType(alternative, alternativeResult));
}
TypeSymbolWithAnnotations resultType;
if (node.HasErrors)
{
resultType = null;
}
else
{
// Determine nested nullability using BestTypeInferrer.
// For constant conditions, we could use the nested nullability of the particular
// 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