// 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 Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp
{
///
/// Nullability flow analysis.
///
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
internal sealed partial class NullableWalker : LocalDataFlowPass
{
///
/// 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
{
// 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 VariableSlot;
internal readonly ImmutableArray VariableBySlot;
internal readonly ImmutableDictionary VariableTypes;
internal VariableState(
ImmutableDictionary variableSlot,
ImmutableArray variableBySlot,
ImmutableDictionary variableTypes)
{
VariableSlot = variableSlot;
VariableBySlot = variableBySlot;
VariableTypes = variableTypes;
}
}
///
/// 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.
///
private readonly struct VisitResult
{
public readonly TypeWithState RValueType;
public readonly TypeSymbolWithAnnotations LValueType;
public VisitResult(TypeWithState resultType, TypeSymbolWithAnnotations lValueType)
{
RValueType = resultType;
LValueType = lValueType;
}
}
///
/// The inferred type at the point of declaration of var locals and parameters.
///
private readonly PooledDictionary _variableTypes = PooledDictionary.GetInstance();
private readonly Binder _binder;
///
/// Conversions with nullability and unknown matching any.
///
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;
///
/// Return statements and the result types from analyzing the returned expressions. Used when inferring lambda return type in MethodTypeInferrer.
///
private readonly ArrayBuilder<(BoundReturnStatement, TypeSymbolWithAnnotations)> _returnTypesOpt;
///
/// 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 TypeWithState _invalidType = new TypeWithState(ErrorTypeSymbol.UnknownResultType, NullableFlowState.NotNull);
///
/// The result and l-value type of the last visited expression.
///
private VisitResult _visitResult;
///
/// The result type represents the state of the last visited expression.
///
private TypeWithState ResultType
{
get => _visitResult.RValueType;
set
{
SetResult(resultType: value, lvalueType: value.ToTypeSymbolWithAnnotations());
}
}
///
/// Force the inference of the LValueResultType from ResultType.
///
private void UseRvalueOnly()
{
ResultType = ResultType;
}
private TypeSymbolWithAnnotations LvalueResultType
{
get => _visitResult.LValueType;
set
{
SetResult(resultType: value.ToTypeWithState(), lvalueType: value);
}
}
///
/// Force the inference of the ResultType from LValueResultType.
///
private void UseLvalueOnly()
{
LvalueResultType = LvalueResultType;
}
private void SetResult(TypeWithState resultType, TypeSymbolWithAnnotations lvalueType)
{
_visitResult = new VisitResult(resultType, lvalueType);
}
///
/// Instances being constructed.
///
private PooledDictionary _placeholderLocalsOpt;
///
/// 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();
_placeholderLocalsOpt?.Free();
base.Free();
}
private NullableWalker(
CSharpCompilation compilation,
MethodSymbol method,
bool useMethodSignatureReturnType,
bool useMethodSignatureParameterTypes,
MethodSymbol methodSignatureOpt,
BoundNode node,
ArrayBuilder<(BoundReturnStatement, TypeSymbolWithAnnotations)> returnTypesOpt,
VariableState initialState,
Action callbackOpt)
: base(compilation, method, node, new EmptyStructTypeCache(compilation, dev12CompilerCompatibility: false), trackUnassignments: true)
{
_callbackOpt = callbackOpt;
_binder = compilation.GetBinderFactory(node.SyntaxTree).GetBinder(node.Syntax);
Debug.Assert(!_binder.Conversions.IncludeNullability);
_conversions = (Conversions)_binder.Conversions.WithNullability(true);
_useMethodSignatureReturnType = (object)methodSignatureOpt != null && useMethodSignatureReturnType;
_useMethodSignatureParameterTypes = (object)methodSignatureOpt != null && useMethodSignatureParameterTypes;
_methodSignatureOpt = methodSignatureOpt;
_returnTypesOpt = returnTypesOpt;
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);
}
}
}
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 Scan(ref bool badRegion)
{
if (_returnTypesOpt != null)
{
_returnTypesOpt.Clear();
}
this.Diagnostics.Clear();
ParameterSymbol methodThisParameter = MethodThisParameter;
this.State = TopState(); // 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 && (!method.IsImplicitConstructor || method.ContainingType.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<(BoundReturnStatement, 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<(BoundReturnStatement, 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)
{
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:
return NullableFlowState.NotNull;
case SymbolKind.Parameter:
{
var parameter = (ParameterSymbol)symbol;
if (parameter.RefKind == RefKind.Out)
{
return NullableFlowState.NotNull;
}
if (!_variableTypes.TryGetValue(parameter, out TypeSymbolWithAnnotations parameterType))
{
parameterType = parameter.Type;
}
return parameterType.ToTypeWithState().State;
}
case SymbolKind.Field:
case SymbolKind.Property:
case SymbolKind.Event:
return symbol.GetTypeOrReturnType().ToTypeWithState().State;
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 = GetBackingFieldIfStructProperty(propSymbol);
if (member is null)
{
return false;
}
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;
}
// https://github.com/dotnet/roslyn/issues/29619 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 && !symbol.ContainingType.IsNullableType())
{
var property = (PropertySymbol)symbol;
var containingType = property.ContainingType;
if (containingType.TypeKind == TypeKind.Struct)
{
// https://github.com/dotnet/roslyn/issues/29619 Relying on field name
// will not work for properties declared in other languages.
var fieldName = GeneratedNames.MakeBackingFieldName(property.Name);
return _emptyStructTypeCache.GetStructFields(containingType, includeStatic: symbol.IsStatic).FirstOrDefault(f => f.Name == fieldName);
}
}
return symbol;
}
// https://github.com/dotnet/roslyn/issues/29619 Temporary, until we're using
// properties on structs directly.
protected override int GetOrCreateSlot(Symbol symbol, int containingSlot = 0)
{
symbol = GetBackingFieldIfStructProperty(symbol);
if (symbol is null)
{
return -1;
}
return base.GetOrCreateSlot(symbol, containingSlot);
}
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:
{
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 to T is equivalent to Nullable.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 _);
}
else if (AreNullableAndUnderlyingTypes(convertedType, operandType, out _))
{
// Explicit conversion of T to Nullable is equivalent to new Nullable(t).
return getPlaceholderSlot(node);
}
}
break;
case ConversionKind.ImplicitNullable:
// Implicit conversion of T to Nullable is equivalent to new Nullable(t).
if (AreNullableAndUnderlyingTypes(conv.Type, conv.Operand.Type, out _))
{
return getPlaceholderSlot(node);
}
break;
case ConversionKind.Identity:
case ConversionKind.ImplicitTupleLiteral:
case ConversionKind.ImplicitTuple:
if (isSupportedConversion(conv.Conversion, conv.Operand))
{
return MakeSlot(conv.Operand);
}
break;
}
}
break;
case BoundKind.DefaultExpression:
case BoundKind.ObjectCreationExpression:
case BoundKind.DynamicObjectCreationExpression:
case BoundKind.AnonymousObjectCreationExpression:
case BoundKind.TupleLiteral:
case BoundKind.ConvertedTupleLiteral:
return getPlaceholderSlot(node);
case BoundKind.ConditionalReceiver:
{
int slot = _lastConditionalAccessSlot;
_lastConditionalAccessSlot = -1;
return slot;
}
default:
// If there was a placeholder local for this node, we should
// use the placeholder for the slot. See other cases above.
Debug.Assert(_placeholderLocalsOpt?.TryGetValue(node, out _) != true);
return base.MakeSlot(node);
}
return -1;
int getPlaceholderSlot(BoundExpression expr)
{
if (_placeholderLocalsOpt != null && _placeholderLocalsOpt.TryGetValue(expr, out ObjectCreationPlaceholderLocal placeholder))
{
return GetOrCreateSlot(placeholder);
}
return -1;
}
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;
}
// Returns true if the nullable state from the operand of the conversion
// can be used as is, and we can create a slot from the conversion.
bool isSupportedConversion(Conversion conversion, BoundExpression operandOpt)
{
// https://github.com/dotnet/roslyn/issues/32599: Allow implicit and explicit
// conversions where the nullable state of the operand remains valid.
switch (conversion.Kind)
{
case ConversionKind.Identity:
case ConversionKind.DefaultOrNullLiteral:
case ConversionKind.ImplicitReference:
return true;
case ConversionKind.ImplicitTupleLiteral:
{
var arguments = ((BoundConvertedTupleLiteral)operandOpt).Arguments;
var conversions = conversion.UnderlyingConversions;
for (int i = 0; i < arguments.Length; i++)
{
// https://github.com/dotnet/roslyn/issues/32600: Copy nullable
// state of tuple elements independently.
if (!isSupportedConversion(conversions[i], (arguments[i] as BoundConversion)?.Operand))
{
return false;
}
}
return true;
}
case ConversionKind.ImplicitTuple:
// https://github.com/dotnet/roslyn/issues/32600: Copy nullable
// state of tuple elements independently.
return conversion.UnderlyingConversions.All(c => isSupportedConversion(c, null));
default:
return false;
}
}
}
protected override void VisitRvalue(BoundExpression node)
{
Visit(node);
Unsplit();
UseRvalueOnly(); // drop lvalue part
}
private TypeWithState VisitRvalueWithState(BoundExpression node)
{
VisitRvalue(node);
return ResultType;
}
private TypeSymbolWithAnnotations VisitLvalueWithAnnotations(BoundExpression node)
{
Visit(node);
Unsplit();
return LvalueResultType;
}
private static object GetTypeAsDiagnosticArgument(TypeSymbol typeOpt)
{
return typeOpt ?? (object)"";
}
private enum AssignmentKind
{
Assignment,
Return,
Argument
}
///
/// Reports top-level nullability problem in assignment.
///
private bool ReportNullableAssignmentIfNecessary(
BoundExpression value,
TypeSymbolWithAnnotations targetType,
TypeWithState valueType,
bool useLegacyWarnings,
AssignmentKind assignmentKind = AssignmentKind.Assignment,
Symbol target = null,
Conversion conversion = default)
{
Debug.Assert((object)target != null || assignmentKind != AssignmentKind.Argument);
if (value == null ||
!targetType.HasType ||
targetType.IsValueType ||
targetType.CanBeAssignedNull ||
valueType.State == NullableFlowState.NotNull)
{
return false;
}
var unwrappedValue = SkipReferenceConversions(value);
if (unwrappedValue.IsSuppressed)
{
return false;
}
HashSet useSiteDiagnostics = null;
if (RequiresSafetyWarningWhenNullIntroduced(targetType.TypeSymbol))
{
if (conversion.Kind == ConversionKind.UnsetConversionKind)
conversion = this._conversions.ClassifyImplicitConversionFromType(valueType.Type, targetType.TypeSymbol, 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))
{
return true;
}
if (assignmentKind == AssignmentKind.Argument)
{
ReportSafetyDiagnostic(ErrorCode.WRN_NullReferenceArgument, value.Syntax,
new FormattedSymbol(target, SymbolDisplayFormat.ShortFormat),
new FormattedSymbol(target.ContainingSymbol, SymbolDisplayFormat.MinimallyQualifiedFormat));
}
else if (useLegacyWarnings)
{
ReportNonSafetyDiagnostic(value.Syntax);
}
else
{
ReportSafetyDiagnostic(assignmentKind == AssignmentKind.Return ? ErrorCode.WRN_NullReferenceReturn : ErrorCode.WRN_NullReferenceAssignment, value.Syntax);
}
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)
{
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(expr.Type))
{
ReportNonSafetyDiagnostic(expr.Syntax);
}
else
{
ReportSafetyDiagnostic(assignmentKind == AssignmentKind.Return ? ErrorCode.WRN_NullReferenceReturn : ErrorCode.WRN_NullAsNonNullable, expr.Syntax);
}
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;
}
}
// Maybe this method can be replaced by VisitOptionalImplicitConversion or ApplyConversion
private void ReportAssignmentWarnings(
BoundExpression value,
TypeSymbolWithAnnotations targetType,
TypeWithState valueType,
bool useLegacyWarnings)
{
Debug.Assert(value != null);
if (this.State.Reachable)
{
if (!targetType.HasType || valueType.HasNullType)
{
return;
}
// Report top-level nullability issues
ReportNullableAssignmentIfNecessary(value, targetType, valueType, useLegacyWarnings, AssignmentKind.Assignment);
// Report nested nullability issues
var sourceType = valueType.Type;
var destinationType = targetType.TypeSymbol;
if ((object)sourceType != null && IsNullabilityMismatch(destinationType, sourceType))
{
ReportNullabilityMismatchInAssignment(value.Syntax, sourceType, destinationType);
}
}
}
private void ReportNullabilityMismatchInAssignment(SyntaxNode syntaxNode, object sourceType, object destinationType)
{
ReportSafetyDiagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, syntaxNode, sourceType, destinationType);
}
///
/// Update tracked value on assignment.
///
private void TrackNullableStateForAssignment(
BoundExpression value,
TypeSymbolWithAnnotations targetType,
int targetSlot,
TypeWithState valueType,
int valueSlot = -1)
{
Debug.Assert(value != null);
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;
this.State[targetSlot] = newState;
if (newState == NullableFlowState.MaybeNull && _tryState.HasValue)
{
var state = _tryState.Value;
state[targetSlot] = NullableFlowState.MaybeNull;
_tryState = state;
}
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)) // https://github.com/dotnet/roslyn/issues/29968 Allow assignment to base type.
{
if (targetType.IsReferenceType || targetType.IsNullableType())
{
// Nullable is handled here rather than in InheritNullableStateOfTrackableStruct since that
// method only clones auto-properties (see https://github.com/dotnet/roslyn/issues/29619).
// When that issue is fixed, Nullable should be handled there instead.
if (valueSlot > 0)
{
InheritNullableStateOfTrackableType(targetSlot, valueSlot, skipSlot: targetSlot);
}
}
else if (EmptyStructTypeCache.IsTrackableStructType(targetType.TypeSymbol))
{
InheritNullableStateOfTrackableStruct(targetType.TypeSymbol, targetSlot, valueSlot, isDefaultValue: IsDefaultValue(value), skipSlot: targetSlot);
}
}
}
bool areEquivalentTypes(TypeSymbolWithAnnotations target, TypeWithState assignedValue) =>
target.TypeSymbol.Equals(assignedValue.Type, TypeCompareKind.AllIgnoreOptions);
}
private void ReportNonSafetyDiagnostic(SyntaxNode syntax)
{
ReportNonSafetyDiagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, syntax);
}
private void ReportNonSafetyDiagnostic(ErrorCode errorCode, SyntaxNode syntax)
{
// All warnings should be in the `#pragma warning ... nullable` set.
Debug.Assert(!ErrorFacts.NullableFlowAnalysisSafetyWarnings.Contains(MessageProvider.Instance.GetIdForErrorCode((int)errorCode)));
Debug.Assert(ErrorFacts.NullableFlowAnalysisNonSafetyWarnings.Contains(MessageProvider.Instance.GetIdForErrorCode((int)errorCode)));
#pragma warning disable CS0618
ReportDiagnostic(errorCode, syntax);
#pragma warning restore CS0618
}
private void ReportSafetyDiagnostic(ErrorCode errorCode, SyntaxNode syntaxNode, params object[] arguments)
{
// All warnings should be in the `#pragma warning ... nullable` set.
Debug.Assert(ErrorFacts.NullableFlowAnalysisSafetyWarnings.Contains(MessageProvider.Instance.GetIdForErrorCode((int)errorCode)));
Debug.Assert(!ErrorFacts.NullableFlowAnalysisNonSafetyWarnings.Contains(MessageProvider.Instance.GetIdForErrorCode((int)errorCode)));
#pragma warning disable CS0618
ReportDiagnostic(errorCode, syntaxNode, arguments);
#pragma warning restore CS0618
}
[Obsolete("Use ReportSafetyDiagnostic/ReportNonSafetyDiagnostic instead", error: false)]
private void ReportDiagnostic(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 isDefaultValue, int skipSlot = -1)
{
Debug.Assert(targetSlot > 0);
Debug.Assert(EmptyStructTypeCache.IsTrackableStructType(targetType));
if (skipSlot < 0)
{
skipSlot = targetSlot;
}
// https://github.com/dotnet/roslyn/issues/29619 Handle properties not backed by fields.
// See ModifyMembers_StructPropertyNoBackingField and PropertyCycle_Struct tests.
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.
TypeSymbolWithAnnotations fieldOrPropertyType = member.GetTypeOrReturnType();
// Nullable is handled here rather than in InheritNullableStateOfTrackableStruct since that
// method only clones auto-properties (see https://github.com/dotnet/roslyn/issues/29619).
// When that issue is fixed, Nullable should be handled there instead.
if (fieldOrPropertyType.TypeSymbol.CanContainNull())
{
int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot);
Debug.Assert(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;
}
this.State[targetMemberSlot] = value;
if (valueMemberSlot > 0)
{
InheritNullableStateOfTrackableType(targetMemberSlot, valueMemberSlot, skipSlot);
}
}
else if (EmptyStructTypeCache.IsTrackableStructType(fieldOrPropertyType.TypeSymbol))
{
int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot);
if (targetMemberSlot > 0)
{
int valueMemberSlot = (valueContainerSlot > 0) ? GetOrCreateSlot(member, valueContainerSlot) : -1;
if (valueMemberSlot == skipSlot)
{
return;
}
InheritNullableStateOfTrackableStruct(fieldOrPropertyType.TypeSymbol, targetMemberSlot, valueMemberSlot, isDefaultValue: isDefaultValue, skipSlot);
}
}
}
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().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 VariableType(variableBySlot[slot].Symbol).TypeSymbol;
}
protected override LocalState TopState()
{
var state = new LocalState(reachable: true, new ArrayBuilder(nextVariableSlot));
Populate(ref state, start: 0);
return state;
}
protected override LocalState UnreachableState()
{
return new LocalState(reachable: false, null);
}
protected override LocalState ReachableBottomState()
{
// Create a reachable state in which all variables are known to be non-null.
var builder = new ArrayBuilder(nextVariableSlot);
builder.AddMany(NullableFlowState.NotNull, nextVariableSlot);
return new LocalState(reachable: true, builder);
}
private void EnterParameters()
{
var methodParameters = ((MethodSymbol)_symbol).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;
EnterParameter(parameter, parameterType);
}
}
private void EnterParameter(ParameterSymbol parameter, TypeSymbolWithAnnotations parameterType)
{
_variableTypes[parameter] = 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,
isDefaultValue: parameter.ExplicitDefaultConstantValue?.IsNull == true);
}
}
}
public override BoundNode VisitIsPatternExpression(BoundIsPatternExpression node)
{
var resultType = VisitRvalueWithState(node.Expression);
VisitPattern(node.Expression, resultType, node.Pattern);
SetNotNullResult(node);
return node;
}
///
/// Examples:
/// `x is Point p`
/// `switch (x) ... case Point p:` // https://github.com/dotnet/roslyn/issues/29873 not yet handled
///
/// If the expression is trackable, we'll return with different null-states for that expression in the two conditional states.
/// If the pattern is a `var` pattern, we'll also have re-inferred the `var` type with nullability and
/// updated the state for that declared local.
///
private void VisitPattern(BoundExpression expression, TypeWithState expressionResultType, BoundPattern pattern)
{
NullableFlowState whenTrue = expressionResultType.State;
NullableFlowState whenFalse = expressionResultType.State;
switch (pattern.Kind)
{
case BoundKind.ConstantPattern:
// If the constant is null, the pattern tells us the expression is null.
// If the constant is not null, the pattern tells us the expression is not null.
// If there is no constant, we don't know.
switch (((BoundConstantPattern)pattern).ConstantValue?.IsNull)
{
case true:
whenTrue = NullableFlowState.MaybeNull;
whenFalse = NullableFlowState.NotNull;
break;
case false:
whenTrue = NullableFlowState.NotNull;
whenFalse = NullableFlowState.MaybeNull;
break;
}
break;
case BoundKind.DeclarationPattern:
var declarationPattern = (BoundDeclarationPattern)pattern;
if (declarationPattern.IsVar)
{
// The result type and state of the expression carry into the variable declared by var pattern
Symbol variable = declarationPattern.Variable;
// No variable declared for discard (`i is var _`)
if ((object)variable != null)
{
var variableType = expressionResultType.ToTypeSymbolWithAnnotations();
_variableTypes[variable] = variableType;
TrackNullableStateForAssignment(expression, variableType, GetOrCreateSlot(variable), expressionResultType);
}
whenFalse = NullableFlowState.NotNull; // whenFalse is unreachable
}
else
{
whenTrue = NullableFlowState.NotNull; // the pattern tells us the expression is not null
}
break;
default:
// https://github.com/dotnet/roslyn/issues/29909 : handle other kinds of patterns
break;
}
Debug.Assert(!IsConditionalState);
// Create slot since EnsureCapacity should be
// called on all fields and that is simpler if state is limited to this.State.
int mainSlot = MakeSlot(expression);
base.VisitPattern(pattern);
Debug.Assert(IsConditionalState);
if (mainSlot > 0)
{
this.StateWhenTrue[mainSlot] = whenTrue;
this.StateWhenFalse[mainSlot] = whenFalse;
}
if (whenTrue == NullableFlowState.NotNull || whenFalse == NullableFlowState.NotNull)
{
var slotBuilder = ArrayBuilder.GetInstance();
GetSlotsToMarkAsNotNullable(expression, slotBuilder);
// Set all nested conditional slots. For example in a?.b?.c we'll set a, b, and c.
if (whenTrue == NullableFlowState.NotNull)
{
MarkSlotsAsNotNull(slotBuilder, ref StateWhenTrue);
}
else if (whenFalse == NullableFlowState.NotNull)
{
MarkSlotsAsNotNull(slotBuilder, ref StateWhenFalse);
}
slotBuilder.Free();
}
}
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 TypeSymbolWithAnnotations returnType))
{
VisitOptionalImplicitConversion(expr, returnType, useLegacyWarnings: false, AssignmentKind.Return);
}
else
{
var result = VisitRvalueWithState(expr);
if (_returnTypesOpt != null)
{
_returnTypesOpt.Add((node, result.ToTypeSymbolWithAnnotations()));
}
}
return null;
}
private bool TryGetReturnType(out TypeSymbolWithAnnotations type)
{
var method = (MethodSymbol)_symbol;
var returnType = (_useMethodSignatureReturnType ? _methodSignatureOpt : method).ReturnType;
Debug.Assert((object)returnType != LambdaSymbol.ReturnTypeIsBeingInferred);
if (returnType.SpecialType == SpecialType.System_Void)
{
type = default;
return false;
}
if (!method.IsAsync)
{
type = returnType;
return true;
}
if (method.IsGenericTaskReturningAsync(compilation))
{
type = ((NamedTypeSymbol)returnType.TypeSymbol).TypeArgumentsNoUseSiteDiagnostics.Single();
return true;
}
type = default;
return false;
}
private static bool RequiresSafetyWarningWhenNullIntroduced(TypeSymbol typeOpt)
{
return typeOpt?.IsTypeParameterDisallowingAnnotation() == true && !typeOpt.IsNullableTypeOrTypeParameter();
}
public override BoundNode VisitLocal(BoundLocal node)
{
var local = node.LocalSymbol;
int slot = GetOrCreateSlot(local);
var type = GetDeclaredLocalResult(local);
SetResult(GetAdjustedResult(type, slot), type);
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;
}
bool inferredType = node.DeclaredType.InferredType;
TypeSymbolWithAnnotations type = local.Type;
TypeWithState valueType = VisitOptionalImplicitConversion(initializer, targetTypeOpt: inferredType ? default : type, useLegacyWarnings: true, AssignmentKind.Assignment);
if (inferredType)
{
if (valueType.HasNullType)
{
Debug.Assert(type.IsErrorType());
valueType = type.ToTypeWithState();
}
type = valueType.ToTypeSymbolWithAnnotations();
_variableTypes[local] = type;
}
TrackNullableStateForAssignment(initializer, type, slot, valueType, MakeSlot(initializer));
return null;
}
protected override BoundExpression VisitExpressionWithoutStackGuard(BoundExpression node)
{
Debug.Assert(!IsConditionalState);
bool wasReachable = this.State.Reachable;
ResultType = _invalidType;
_ = 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
_callbackOpt?.Invoke(node, resultType.ToTypeSymbolWithAnnotations());
if (node.IsSuppressed || node.HasAnyErrors || !wasReachable)
{
var result = resultType.WithNotNullState();
SetResult(result, LvalueResultType);
}
return null;
}
#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 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.GetAnonymousTypePropertyTypes(type).Any(t => canIgnoreAnyType(t.TypeSymbol));
}
}
#endif
protected override void VisitStatement(BoundStatement statement)
{
ResultType = _invalidType;
base.VisitStatement(statement);
ResultType = _invalidType;
}
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 arguments,
ImmutableArray argumentResults,
BoundExpression initializerOpt)
{
Debug.Assert(node.Kind == BoundKind.ObjectCreationExpression || node.Kind == BoundKind.DynamicObjectCreationExpression);
var argumentTypes = argumentResults.SelectAsArray(ar => ar.RValueType);
int slot = -1;
TypeSymbol type = node.Type;
NullableFlowState resultState = NullableFlowState.NotNull;
if ((object)type != null)
{
bool isTrackableStructType = EmptyStructTypeCache.IsTrackableStructType(type);
var constructor = (node as BoundObjectCreationExpression)?.Constructor;
bool isDefaultValueTypeConstructor = constructor?.IsDefaultValueTypeConstructor() == true;
if (!type.IsValueType || isTrackableStructType)
{
slot = GetOrCreateObjectCreationPlaceholderSlot(node);
if (slot > 0 && isTrackableStructType)
{
this.State[slot] = NullableFlowState.NotNull;
var tupleType = constructor?.ContainingType as TupleTypeSymbol;
if ((object)tupleType != null && !isDefaultValueTypeConstructor)
{
// new System.ValueTuple(e1, ..., eN)
TrackNullableStateOfTupleElements(slot, tupleType, arguments, argumentTypes, useRestField: true);
}
else
{
InheritNullableStateOfTrackableStruct(
type,
slot,
valueSlot: -1,
isDefaultValue: isDefaultValueTypeConstructor);
}
}
}
else if (type.IsNullableType() && isDefaultValueTypeConstructor)
{
// a nullable value type created with its default constructor is by definition null
resultState = NullableFlowState.MaybeNull;
}
}
if (initializerOpt != null)
{
VisitObjectCreationInitializer(null, slot, initializerOpt);
}
ResultType = new TypeWithState(type, resultState);
}
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(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:
TypeWithState resultType = VisitRvalueWithState(node);
Debug.Assert((object)containingSymbol != null);
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(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:
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);
SetUnknownResultNullability();
}
private void SetNotNullResult(BoundExpression node)
{
ResultType = new TypeWithState(node.Type, NullableFlowState.NotNull);
}
private int GetOrCreateObjectCreationPlaceholderSlot(BoundExpression node)
{
ObjectCreationPlaceholderLocal placeholder;
if (_placeholderLocalsOpt == null)
{
_placeholderLocalsOpt = PooledDictionary.GetInstance();
placeholder = null;
}
else
{
_placeholderLocalsOpt.TryGetValue(node, out placeholder);
}
if (placeholder is null)
{
placeholder = new ObjectCreationPlaceholderLocal(_symbol, node);
_placeholderLocalsOpt.Add(node, placeholder);
}
return GetOrCreateSlot(placeholder);
}
public override BoundNode VisitAnonymousObjectCreationExpression(BoundAnonymousObjectCreationExpression node)
{
Debug.Assert(!IsConditionalState);
var anonymousType = (NamedTypeSymbol)node.Type;
Debug.Assert(anonymousType.IsAnonymousType);
var arguments = node.Arguments;
var argumentTypes = arguments.SelectAsArray((arg, self) =>
self.VisitRvalueWithState(arg), this);
var argumentsWithAnnotations = argumentTypes.SelectAsArray((arg, self) =>
arg.ToTypeSymbolWithAnnotations().AsSpeakable(), this);
if (argumentsWithAnnotations.All(argType => argType.HasType))
{
anonymousType = AnonymousTypeManager.ConstructAnonymousTypeSymbol(anonymousType, argumentsWithAnnotations);
int receiverSlot = GetOrCreateObjectCreationPlaceholderSlot(node);
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.Type, GetOrCreateSlot(property, receiverSlot), argumentType, MakeSlot(argument));
}
}
ResultType = new TypeWithState(anonymousType, NullableFlowState.NotNull);
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 = new TypeWithState(resultType, NullableFlowState.NotNull);
return null;
}
private ArrayTypeSymbol VisitArrayInitializer(BoundArrayCreation node)
{
BoundArrayInitialization initialization = node.InitializerOpt;
var expressions = ArrayBuilder.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.ElementType;
if (!isInferred)
{
for (int i = 0; i < n; i++)
{
_ = VisitOptionalImplicitConversion(expressions[i], elementType, useLegacyWarnings: false, AssignmentKind.Assignment);
}
ResultType = _invalidType;
return arrayType;
}
var conversions = ArrayBuilder.GetInstance(n);
var resultTypes = ArrayBuilder.GetInstance(n);
for (int i = 0; i < n; i++)
{
// collect expressions, conversions and result types
(BoundExpression expression, Conversion conversion) = RemoveConversion(expressions[i], includeExplicitConversions: false);
expressions[i] = expression;
conversions.Add(conversion);
var resultType = VisitRvalueWithState(expression);
resultTypes.Add(resultType);
}
var placeholderBuilder = ArrayBuilder.GetInstance(n);
for (int i = 0; i < n; i++)
{
placeholderBuilder.Add(CreatePlaceholderIfNecessary(expressions[i], resultTypes[i].ToTypeSymbolWithAnnotations()));
}
var placeholders = placeholderBuilder.ToImmutableAndFree();
TypeSymbol bestType = null;
if (!node.HasErrors)
{
HashSet useSiteDiagnostics = null;
bestType = BestTypeInferrer.InferBestType(placeholders, _conversions, ref useSiteDiagnostics);
}
TypeSymbolWithAnnotations inferredType;
if (bestType is null)
{
inferredType = elementType.SetUnknownNullabilityForReferenceTypes();
}
else
{
inferredType = TypeSymbolWithAnnotations.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 placeholder = placeholders[i];
resultTypes[i] = ApplyConversion(placeholder, placeholder, 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
inferredType = TypeSymbolWithAnnotations.Create(inferredType.TypeSymbol, BestTypeInferrer.GetNullableAnnotation(resultTypes)).AsSpeakable();
for (int i = 0; i < n; i++)
{
var nodeForSyntax = expressions[i];
// Report top-level warnings
_ = ApplyConversion(nodeForSyntax, operandOpt: nodeForSyntax, Conversion.Identity, targetTypeWithNullability: inferredType, operandType: resultTypes[i],
checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: false);
}
}
resultTypes.Free();
expressions.Free();
ResultType = _invalidType;
arrayType = arrayType.WithElementType(inferredType);
return arrayType;
}
///
/// Applies a method similar to
/// 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.
///
internal static TypeSymbolWithAnnotations BestTypeForLambdaReturns(
ArrayBuilder<(BoundExpression, TypeSymbolWithAnnotations)> returns,
CSharpCompilation compilation,
BoundNode node)
{
var walker = new NullableWalker(compilation, method: null,
useMethodSignatureReturnType: false, useMethodSignatureParameterTypes: false, methodSignatureOpt: null,
node, returnTypesOpt: null, initialState: null, callbackOpt: null);
int n = returns.Count;
var expressions = ArrayBuilder.GetInstance();
var resultTypes = ArrayBuilder.GetInstance(n);
var placeholdersBuilder = ArrayBuilder.GetInstance(n);
for (int i = 0; i < n; i++)
{
var (returnExpr, resultType) = returns[i];
expressions.Add(returnExpr);
resultTypes.Add(resultType);
placeholdersBuilder.Add(CreatePlaceholderIfNecessary(returnExpr, resultType));
}
HashSet useSiteDiagnostics = null;
var placeholders = placeholdersBuilder.ToImmutableAndFree();
TypeSymbol bestType = BestTypeInferrer.InferBestType(placeholders, walker._conversions, ref useSiteDiagnostics);
TypeSymbolWithAnnotations inferredType;
if ((object)bestType != null)
{
// Note: so long as we have a best type, we can proceed.
var bestTypeWithObliviousAnnotation = TypeSymbolWithAnnotations.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.ApplyConversion(placeholder, placeholder, conversion, bestTypeWithObliviousAnnotation, resultTypes[i].ToTypeWithState(),
checkConversion: false, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Return,
reportRemainingWarnings: false, reportTopLevelWarnings: false).ToTypeSymbolWithAnnotations();
}
// Set top-level nullability on inferred type
inferredType = TypeSymbolWithAnnotations.Create(bestType, BestTypeInferrer.GetNullableAnnotation(resultTypes));
}
else
{
inferredType = default;
}
resultTypes.Free();
expressions.Free();
walker.Free();
return inferredType;
}
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);
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);
}
TypeSymbolWithAnnotations result;
if (node.Indices.Length == 1 &&
TypeSymbol.Equals(node.Indices[0].Type, compilation.GetWellKnownType(WellKnownType.System_Range), TypeCompareKind.ConsiderEverything2))
{
result = TypeSymbolWithAnnotations.Create(type);
}
else
{
result = type?.ElementType ?? default;
}
LvalueResultType = 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.ReturnType, leftType.State.JoinForFlowAnalysisBranches(rightType.State))
: methodOpt.ReturnType.ToTypeWithState();
}
}
else if (!operatorKind.IsDynamic() && !resultType.IsValueType)
{
switch (operatorKind.Operator() | operatorKind.OperandTypes())
{
case BinaryOperatorKind.DelegateCombination:
resultState = leftType.State.MeetForFlowAnalysisFinally(rightType.State);
break;
case BinaryOperatorKind.DelegateRemoval:
resultState = NullableFlowState.MaybeNull; // Delegate removal can produce null.
break;
default:
resultState = NullableFlowState.NotNull;
break;
}
}
if (operatorKind.IsLifted())
{
resultState = leftType.State.JoinForFlowAnalysisBranches(rightType.State);
}
return new TypeWithState(resultType, resultState);
}
protected override void AfterLeftChildHasBeenVisited(BoundBinaryOperator binary)
{
Debug.Assert(!IsConditionalState);
//if (this.State.Reachable) // Consider reachability: see https://github.com/dotnet/roslyn/issues/28798
{
TypeWithState 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]);
}
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 (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;
TypeWithState operandComparedToNullType = default;
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)
{
// Skip reference conversions
operandComparedToNull = SkipReferenceConversions(operandComparedToNull);
// Set all nested conditional slots. For example in a?.b?.c we'll set a, b, and c.
var slotBuilder = ArrayBuilder.GetInstance();
GetSlotsToMarkAsNotNullable(operandComparedToNull, slotBuilder);
if (slotBuilder.Count != 0)
{
Split();
ref LocalState stateToUpdate = ref (op == BinaryOperatorKind.Equal) ? ref this.StateWhenFalse : ref this.StateWhenTrue;
MarkSlotsAsNotNull(slotBuilder, ref stateToUpdate);
}
slotBuilder.Free();
}
}
}
}
///
/// 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.
///
private void GetSlotsToMarkAsNotNullable(BoundExpression operand, ArrayBuilder slotBuilder)
{
Debug.Assert(operand != null);
Debug.Assert(_lastConditionalAccessSlot == -1);
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/29953 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);
// 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/29953 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))
{
// 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;
}
}
private static bool PossiblyNullableType(TypeSymbol operandType) => operandType?.CanContainNull() == true;
private static void MarkSlotsAsNotNull(ArrayBuilder 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.GetInstance();
GetSlotsToMarkAsNotNullable(expression, slotBuilder);
MarkSlotsAsNotNull(slotBuilder, ref state);
slotBuilder.Free();
}
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
TypeSymbolWithAnnotations targetType = VisitLvalueWithAnnotations(leftOperand);
TypeWithState currentLeftType = GetAdjustedResult(targetType, leftSlot);
var leftState = this.State.Clone();
TypeWithState rightResult = VisitOptionalImplicitConversion(rightOperand, targetType, UseLegacyWarnings(leftOperand), AssignmentKind.Assignment);
TrackNullableStateForAssignment(rightOperand, targetType, leftSlot, rightResult, MakeSlot(rightOperand));
Join(ref this.State, ref leftState);
TypeWithState resultType = GetNullCoalescingResultType(leftOperand, currentLeftType, rightOperand, rightResult, targetType.TypeSymbol);
ResultType = resultType;
if (leftSlot > 0)
{
this.State[leftSlot] = resultType.State;
}
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.
ResultType = new TypeWithState(node.Type, getNullableState(rightOperand, rightResult));
return null;
}
var whenNotNull = this.State.Clone();
LearnFromNonNullTest(leftOperand, ref whenNotNull);
// Consider learning in whenNull branch as well
// https://github.com/dotnet/roslyn/issues/30297
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);
}
ResultType = GetNullCoalescingResultType(leftOperand, leftResult, rightOperand, rightResult, resultType);
return null;
NullableFlowState getNullableState(BoundExpression e, TypeWithState t)
{
if (t.HasNullType)
{
return GetNullableAnnotation(e).IsAnyNullable() ? NullableFlowState.MaybeNull : NullableFlowState.NotNull;
}
return t.State;
}
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;
}
}
///
/// 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 which is an expression
/// specifically created in NullableWalker to represent the flow analysis state.
///
private static NullableAnnotation GetNullableAnnotation(BoundExpression expr)
{
switch (expr.Kind)
{
case BoundKind.DefaultExpression:
case BoundKind.Literal:
{
return (expr.ConstantValue?.IsNull != false) ? NullableAnnotation.NotNullable : NullableAnnotation.Nullable;
}
case BoundKind.ExpressionWithNullability:
return ((BoundExpressionWithNullability)expr).NullableAnnotation;
case BoundKind.MethodGroup:
case BoundKind.UnboundLambda:
return NullableAnnotation.NotNullable;
default:
Debug.Assert(false); // unexpected value
return NullableAnnotation.Unknown;
}
}
private static TypeWithState GetNullCoalescingResultType(BoundExpression leftOperand, TypeWithState leftResult, BoundExpression rightOperand, TypeWithState rightResult, TypeSymbol resultType)
{
NullableFlowState resultState;
if (getNullableState(leftOperand, leftResult) == NullableFlowState.NotNull)
{
resultState = getNullableState(leftOperand, leftResult);
}
else
{
resultState = getNullableState(rightOperand, rightResult);
}
return new TypeWithState(resultType, resultState);
NullableFlowState getNullableState(BoundExpression e, TypeWithState t)
{
if (t.HasNullType)
{
return GetNullableAnnotation(e).IsAnyNullable() ? NullableFlowState.MaybeNull : NullableFlowState.NotNull;
}
return t.State;
}
}
public override BoundNode VisitConditionalAccess(BoundConditionalAccess node)
{
Debug.Assert(!IsConditionalState);
var receiver = node.Receiver;
var receiverType = VisitRvalueWithState(receiver);
var receiverState = this.State.Clone();
if (IsConstantNull(node.Receiver))
{
SetUnreachable();
}
else
{
// In the right-hand-side, the left-hand-side is known to be non-null.
// https://github.com/dotnet/roslyn/issues/33347: This should probably be using GetSlotsToMarkAsNotNullable() rather than marking only one slot
int slot = MakeSlot(SkipReferenceConversions(receiver));
if (slot > 0)
{
if (slot >= this.State.Capacity) Normalize(ref this.State);
this.State[slot] = NullableFlowState.NotNull;
}
}
var accessExpressionType = VisitRvalueWithState(node.AccessExpression);
Join(ref this.State, ref receiverState);
// Per LDM 2019-02-13 decision, the result of a conditional access might be null even if
// both the receiver and right-hand-side are believed not to be null.
NullableFlowState resultState = NullableFlowState.MaybeNull;
// https://github.com/dotnet/roslyn/issues/29956 Use flow analysis type rather than node.Type
// so that nested nullability is inferred from flow analysis. See VisitConditionalOperator.
TypeSymbol type = node.Type;
// If the result type does not allow annotations, then we produce a warning because
// the result may be null.
if (RequiresSafetyWarningWhenNullIntroduced(type))
{
ReportSafetyDiagnostic(ErrorCode.WRN_ConditionalAccessMayReturnNull, node.Syntax, node.Type);
}
ResultType = new TypeWithState(type, resultState);
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;
Conversion consequenceConversion;
Conversion alternativeConversion;
TypeSymbolWithAnnotations consequenceResult;
TypeSymbolWithAnnotations alternativeResult;
bool isConstantTrue = IsConstantTrue(node.Condition);
bool isConstantFalse = IsConstantFalse(node.Condition);
if (isConstantTrue)
{
(alternative, alternativeConversion, alternativeResult) = visitConditionalOperand(alternativeState, node.Alternative);
(consequence, consequenceConversion, consequenceResult) = visitConditionalOperand(consequenceState, node.Consequence);
}
else if (isConstantFalse)
{
(consequence, consequenceConversion, consequenceResult) = visitConditionalOperand(consequenceState, node.Consequence);
(alternative, alternativeConversion, alternativeResult) = visitConditionalOperand(alternativeState, node.Alternative);
}
else
{
(consequence, consequenceConversion, consequenceResult) = visitConditionalOperand(consequenceState, node.Consequence);
Unsplit();
(alternative, alternativeConversion, alternativeResult) = visitConditionalOperand(alternativeState, node.Alternative);
Unsplit();
Join(ref this.State, ref consequenceState);
}
TypeSymbol 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