// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #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.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { /// /// 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; // The nullable state of all variables captured at the point where the function or lambda appeared. internal readonly LocalState VariableNullableStates; internal VariableState( ImmutableDictionary variableSlot, ImmutableArray variableBySlot, ImmutableDictionary variableTypes, LocalState variableNullableStates) { VariableSlot = variableSlot; VariableBySlot = variableBySlot; VariableTypes = variableTypes; VariableNullableStates = variableNullableStates; } internal VariableState Clone() { return new VariableState(VariableSlot, VariableBySlot, VariableTypes, VariableNullableStates.Clone()); } } /// /// 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. /// [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] private readonly struct VisitResult { public readonly TypeWithState RValueType; public readonly TypeWithAnnotations LValueType; public VisitResult(TypeWithState rValueType, TypeWithAnnotations lValueType) { RValueType = rValueType; LValueType = lValueType; // https://github.com/dotnet/roslyn/issues/34993: Doesn't hold true for Tuple_Assignment_10. See if we can make it hold true //Debug.Assert((RValueType.Type is null && LValueType.TypeSymbol is null) || // RValueType.Type.Equals(LValueType.TypeSymbol, TypeCompareKind.ConsiderEverything | TypeCompareKind.AllIgnoreOptions)); } public VisitResult(TypeSymbol? type, NullableAnnotation annotation, NullableFlowState state) { RValueType = TypeWithState.Create(type, state); LValueType = TypeWithAnnotations.Create(type, annotation); Debug.Assert(TypeSymbol.Equals(RValueType.Type, LValueType.Type, TypeCompareKind.ConsiderEverything)); } internal string GetDebuggerDisplay() => $"{{LValue: {LValueType.GetDebuggerDisplay()}, RValue: {RValueType.GetDebuggerDisplay()}}}"; } /// /// Represents the result of visiting an argument expression. /// In addition to storing the , also stores the /// for reanalyzing a lambda. /// [DebuggerDisplay("{VisitResult.GetDebuggerDisplay(), nq}")] private readonly struct VisitArgumentResult { public readonly VisitResult VisitResult; public readonly Optional StateForLambda; public TypeWithState RValueType => VisitResult.RValueType; public TypeWithAnnotations LValueType => VisitResult.LValueType; public VisitArgumentResult(VisitResult visitResult, Optional stateForLambda) { VisitResult = visitResult; StateForLambda = stateForLambda; } } /// /// The inferred type at the point of declaration of var locals and parameters. /// private readonly PooledDictionary _variableTypes = SpecializedSymbolCollections.GetPooledSymbolDictionaryInstance(); /// /// Binder for symbol being analyzed. /// Must be non-null except from BestTypeForLambdaReturns which does not rely on this field. /// private readonly Binder? _binder; /// /// Conversions with nullability and unknown matching any. /// private readonly Conversions _conversions; /// /// 'true' if non-nullable member warnings should be issued at return points. /// One situation where this is 'false' is when we are analyzing field initializers and there is a constructor symbol in the type. /// private readonly bool _useConstructorExitWarnings; /// /// If true, the parameter types and nullability from _delegateInvokeMethod is used for /// initial parameter state. If false, the signature of CurrentSymbol is used instead. /// private bool _useDelegateInvokeParameterTypes; /// /// Method signature used for return or parameter types. Distinct from CurrentSymbol signature /// when CurrentSymbol is a lambda and type is inferred from MethodTypeInferrer. /// private MethodSymbol? _delegateInvokeMethod; /// /// Return statements and the result types from analyzing the returned expressions. Used when inferring lambda return type in MethodTypeInferrer. /// private ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)>? _returnTypesOpt; /// /// Invalid type, used only to catch Visit methods that do not set /// _result.Type. See VisitExpressionWithoutStackGuard. /// private static readonly TypeWithState _invalidType = TypeWithState.Create(ErrorTypeSymbol.UnknownResultType, NullableFlowState.NotNull); /// /// Contains the map of expressions to inferred nullabilities and types used by the optional rewriter phase of the /// compiler. /// private readonly ImmutableDictionary.Builder? _analyzedNullabilityMapOpt; /// /// Manages creating snapshots of the walker as appropriate. Null if we're not taking snapshots of /// this walker. /// private readonly SnapshotManager.Builder? _snapshotBuilderOpt; // https://github.com/dotnet/roslyn/issues/35043: remove this when all expression are supported private bool _disableNullabilityAnalysis; /// /// State of method group receivers, used later when analyzing the conversion to a delegate. /// (Could be replaced by _analyzedNullabilityMapOpt if that map is always available.) /// private PooledDictionary? _methodGroupReceiverMapOpt; /// /// State of awaitable expressions, for substitution in placeholders within GetAwaiter calls. /// private PooledDictionary? _awaitablePlaceholdersOpt; /// /// True if we're analyzing speculative code. This turns off some initialization steps /// that would otherwise be taken. /// private readonly bool _isSpeculative; /// /// True if this walker was created using an initial state. /// private readonly bool _hasInitialState; #if DEBUG /// /// Contains the expressions that should not be inserted into . /// private static readonly ImmutableArray s_skippedExpressions = ImmutableArray.Create(BoundKind.ArrayInitialization, BoundKind.ObjectInitializerExpression, BoundKind.CollectionInitializerExpression, BoundKind.DynamicCollectionElementInitializer); #endif /// /// The result and l-value type of the last visited expression. /// private VisitResult _visitResult; /// /// The visit result of the receiver for the current conditional access. /// /// For example: A conditional invocation uses a placeholder as a receiver. By storing the /// visit result from the actual receiver ahead of time, we can give this placeholder a correct result. /// private VisitResult _currentConditionalReceiverVisitResult; /// /// Gets the synthesized default argument expression for the given syntax node and parameter. /// Upon re-analysis, the bound nodes that get visited must have the same identity as the previous analysis pass /// so that the analysis does not believe that new variables were found each time and repeat indefinitely. /// /// Each call which implicitly passes a default value needs its own synthesized BoundExpression /// because the location of the call can affect the default parameter value. /// Therefore the dictionary key must be (SyntaxNode, ParameterSymbol) instead of just ParameterSymbol. /// private PooledDictionary<(SyntaxNode, ParameterSymbol), BoundExpression>? _defaultValuesOpt; /// /// The result type represents the state of the last visited expression. /// private TypeWithState ResultType { get => _visitResult.RValueType; } private void SetResultType(BoundExpression? expression, TypeWithState type, bool updateAnalyzedNullability = true) { SetResult(expression, resultType: type, lvalueType: type.ToTypeWithAnnotations(compilation), updateAnalyzedNullability: updateAnalyzedNullability); } /// /// Force the inference of the LValueResultType from ResultType. /// private void UseRvalueOnly(BoundExpression? expression) { SetResult(expression, ResultType, ResultType.ToTypeWithAnnotations(compilation), isLvalue: false); } private TypeWithAnnotations LvalueResultType { get => _visitResult.LValueType; } private void SetLvalueResultType(BoundExpression? expression, TypeWithAnnotations type) { SetResult(expression, resultType: type.ToTypeWithState(), lvalueType: type); } /// /// Force the inference of the ResultType from LValueResultType. /// private void UseLvalueOnly(BoundExpression? expression) { SetResult(expression, LvalueResultType.ToTypeWithState(), LvalueResultType, isLvalue: true); } private void SetInvalidResult() => SetResult(expression: null, _invalidType, _invalidType.ToTypeWithAnnotations(compilation), updateAnalyzedNullability: false); private void SetResult(BoundExpression? expression, TypeWithState resultType, TypeWithAnnotations lvalueType, bool updateAnalyzedNullability = true, bool? isLvalue = null) { _visitResult = new VisitResult(resultType, lvalueType); if (updateAnalyzedNullability) { SetAnalyzedNullability(expression, _visitResult, isLvalue); } } private bool ShouldMakeNotNullRvalue(BoundExpression node) => node.IsSuppressed || node.HasAnyErrors || !IsReachable(); /// /// Sets the analyzed nullability of the expression to be the given result. /// private void SetAnalyzedNullability(BoundExpression? expr, VisitResult result, bool? isLvalue = null) { if (expr == null || _disableNullabilityAnalysis) return; #if DEBUG // https://github.com/dotnet/roslyn/issues/34993: This assert is essential for ensuring that we aren't // changing the observable results of GetTypeInfo beyond nullability information. //Debug.Assert(AreCloseEnough(expr.Type, result.RValueType.Type), // $"Cannot change the type of {expr} from {expr.Type} to {result.RValueType.Type}"); #endif if (_analyzedNullabilityMapOpt != null) { // https://github.com/dotnet/roslyn/issues/34993: enable and verify these assertions #if false if (_analyzedNullabilityMapOpt.TryGetValue(expr, out var existing)) { if (!(result.RValueType.State == NullableFlowState.NotNull && ShouldMakeNotNullRvalue(expr, State.Reachable))) { switch (isLvalue) { case true: Debug.Assert(existing.Info.Annotation == result.LValueType.NullableAnnotation.ToPublicAnnotation(), $"Tried to update the nullability of {expr} from {existing.Info.Annotation} to {result.LValueType.NullableAnnotation}"); break; case false: Debug.Assert(existing.Info.FlowState == result.RValueType.State, $"Tried to update the nullability of {expr} from {existing.Info.FlowState} to {result.RValueType.State}"); break; case null: Debug.Assert(existing.Info.Equals((NullabilityInfo)result), $"Tried to update the nullability of {expr} from ({existing.Info.Annotation}, {existing.Info.FlowState}) to ({result.LValueType.NullableAnnotation}, {result.RValueType.State})"); break; } } } #endif _analyzedNullabilityMapOpt[expr] = (new NullabilityInfo(result.LValueType.ToPublicAnnotation(), result.RValueType.State.ToPublicFlowState()), // https://github.com/dotnet/roslyn/issues/35046 We're dropping the result if the type doesn't match up completely // with the existing type expr.Type?.Equals(result.RValueType.Type, TypeCompareKind.AllIgnoreOptions) == true ? result.RValueType.Type : expr.Type); } } /// /// Placeholder locals, e.g. for objects 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; /// /// Whether we are going to read the currently visited expression. /// private bool _expressionIsRead = true; /// /// Used to allow to substitute the correct slot for a when /// it's encountered. /// private int _lastConditionalAccessSlot = -1; private bool IsAnalyzingAttribute => methodMainNode.Kind == BoundKind.Attribute; protected override void Free() { _awaitablePlaceholdersOpt?.Free(); _methodGroupReceiverMapOpt?.Free(); _variableTypes.Free(); _placeholderLocalsOpt?.Free(); _defaultValuesOpt?.Free(); base.Free(); } private NullableWalker( CSharpCompilation compilation, Symbol? symbol, bool useConstructorExitWarnings, bool useDelegateInvokeParameterTypes, MethodSymbol? delegateInvokeMethodOpt, BoundNode node, Binder? binder, Conversions conversions, VariableState? initialState, ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)>? returnTypesOpt, ImmutableDictionary.Builder? analyzedNullabilityMapOpt, SnapshotManager.Builder? snapshotBuilderOpt, bool isSpeculative = false) // Members of variables are tracked up to a fixed depth, to avoid cycles. The // maxSlotDepth value is arbitrary but large enough to allow most scenarios. : base(compilation, symbol, node, EmptyStructTypeCache.CreatePrecise(), trackUnassignments: true, maxSlotDepth: 5) { Debug.Assert(!useDelegateInvokeParameterTypes || delegateInvokeMethodOpt is object); _binder = binder; _conversions = (Conversions)conversions.WithNullability(true); _useConstructorExitWarnings = useConstructorExitWarnings; _useDelegateInvokeParameterTypes = useDelegateInvokeParameterTypes; _delegateInvokeMethod = delegateInvokeMethodOpt; _analyzedNullabilityMapOpt = analyzedNullabilityMapOpt; _returnTypesOpt = returnTypesOpt; _snapshotBuilderOpt = snapshotBuilderOpt; _isSpeculative = isSpeculative; if (initialState != null) { _hasInitialState = true; 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 (key, value) in initialState.VariableTypes) { _variableTypes.Add(key, value); } this.State = initialState.VariableNullableStates.Clone(); } else { _hasInitialState = false; } } 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(); this.regionPlace = RegionPlace.Before; if (!_isSpeculative) { ParameterSymbol methodThisParameter = MethodThisParameter; EnterParameters(); // assign parameters if (methodThisParameter is object) { EnterParameter(methodThisParameter, methodThisParameter.TypeWithAnnotations); } makeNotNullMembersMaybeNull(); // We need to create a snapshot even of the first node, because we want to have the state of the initial parameters. _snapshotBuilderOpt?.TakeIncrementalSnapshot(methodMainNode, State); } ImmutableArray pendingReturns = base.Scan(ref badRegion); if ((_symbol as MethodSymbol)?.IsConstructor() != true || _useConstructorExitWarnings) { EnforceDoesNotReturn(syntaxOpt: null); enforceMemberNotNull(syntaxOpt: null, this.State); enforceNotNull(null, this.State); foreach (var pendingReturn in pendingReturns) { enforceMemberNotNull(syntaxOpt: pendingReturn.Branch.Syntax, pendingReturn.State); if (pendingReturn.Branch is BoundReturnStatement returnStatement) { enforceNotNull(returnStatement.Syntax, pendingReturn.State); enforceNotNullWhenForPendingReturn(pendingReturn, returnStatement); enforceMemberNotNullWhenForPendingReturn(pendingReturn, returnStatement); } } } return pendingReturns; void enforceMemberNotNull(SyntaxNode? syntaxOpt, LocalState state) { if (!state.Reachable) { return; } var method = _symbol as MethodSymbol; if (method is object) { if (method.IsConstructor()) { Debug.Assert(_useConstructorExitWarnings); var thisSlot = 0; if (method.RequiresInstanceReceiver) { method.TryGetThisParameter(out var thisParameter); Debug.Assert(thisParameter is object); thisSlot = GetOrCreateSlot(thisParameter); } // https://github.com/dotnet/roslyn/issues/46718: give diagnostics on return points, not constructor signature var exitLocation = method.DeclaringSyntaxReferences.IsEmpty ? null : method.Locations.FirstOrDefault(); foreach (var member in method.ContainingType.GetMembersUnordered()) { checkMemberStateOnConstructorExit(method, member, state, thisSlot, exitLocation); } } else { do { foreach (var memberName in method.NotNullMembers) { enforceMemberNotNullOnMember(syntaxOpt, state, method, memberName); } method = method.OverriddenMethod; } while (method != null); } } } void checkMemberStateOnConstructorExit(MethodSymbol constructor, Symbol member, LocalState state, int thisSlot, Location? exitLocation) { var isStatic = !constructor.RequiresInstanceReceiver(); if (member.IsStatic != isStatic) { return; } // This is not required for correctness, but in the case where the member has // an initializer, we know we've assigned to the member and // have given any applicable warnings about a bad value going in. // Therefore we skip this check when the member has an initializer to reduce noise. if (HasInitializer(member)) { return; } TypeWithAnnotations fieldType; FieldSymbol? field; Symbol symbol; switch (member) { case FieldSymbol f: fieldType = f.TypeWithAnnotations; field = f; symbol = (Symbol?)(f.AssociatedSymbol as PropertySymbol) ?? f; break; case EventSymbol e: fieldType = e.TypeWithAnnotations; field = e.AssociatedField; symbol = e; if (field is null) { return; } break; default: return; } if (field.IsConst) { return; } if (fieldType.Type.IsValueType || fieldType.Type.IsErrorType()) { return; } if (!fieldType.NullableAnnotation.IsNotAnnotated()) { return; } var annotations = symbol.GetFlowAnalysisAnnotations(); if ((annotations & (FlowAnalysisAnnotations.MaybeNull | FlowAnalysisAnnotations.AllowNull)) != 0) { // We assume that if a member has MaybeNull or AllowNull then the user // does not care that we exit at a point where the member might be null. return; } var slot = GetOrCreateSlot(symbol, thisSlot); if (slot < 0) { return; } var memberState = state[slot]; var badState = fieldType.Type.IsPossiblyNullableReferenceTypeTypeParameter() ? NullableFlowState.MaybeDefault : NullableFlowState.MaybeNull; if (memberState == badState) { Diagnostics.Add(ErrorCode.WRN_UninitializedNonNullableField, exitLocation ?? symbol.Locations.FirstOrNone(), symbol.Kind.Localize(), symbol.Name); } } void enforceMemberNotNullOnMember(SyntaxNode? syntaxOpt, LocalState state, MethodSymbol method, string memberName) { foreach (var member in method.ContainingType.GetMembers(memberName)) { if (memberHasBadState(member, state)) { // Member '{name}' must have a non-null value when exiting. Diagnostics.Add(ErrorCode.WRN_MemberNotNull, syntaxOpt?.GetLocation() ?? methodMainNode.Syntax.GetLastToken().GetLocation(), member.Name); } } } void enforceMemberNotNullWhenForPendingReturn(PendingBranch pendingReturn, BoundReturnStatement returnStatement) { if (pendingReturn.IsConditionalState) { enforceMemberNotNullWhen(returnStatement.Syntax, sense: true, pendingReturn.StateWhenTrue); enforceMemberNotNullWhen(returnStatement.Syntax, sense: false, pendingReturn.StateWhenFalse); } } void enforceMemberNotNullWhen(SyntaxNode? syntaxOpt, bool sense, LocalState state) { if (!state.Reachable) { return; } if (_symbol is MethodSymbol method) { var notNullMembers = sense ? method.NotNullWhenTrueMembers : method.NotNullWhenFalseMembers; foreach (var memberName in notNullMembers) { foreach (var member in method.ContainingType.GetMembers(memberName)) { if (memberHasBadState(member, state)) { // Member '{name}' must have a non-null value when exiting with '{sense}'. Diagnostics.Add(ErrorCode.WRN_MemberNotNullWhen, syntaxOpt?.GetLocation() ?? methodMainNode.Syntax.GetLastToken().GetLocation(), member.Name, sense ? "true" : "false"); } } } } } bool memberHasBadState(Symbol member, LocalState state) { switch (member.Kind) { case SymbolKind.Field: case SymbolKind.Property: if (getSlotForFieldOrPropertyOrEvent(member) is int memberSlot && memberSlot > 0) { var parameterState = state[memberSlot]; return !parameterState.IsNotNull(); } else { return false; } case SymbolKind.Event: case SymbolKind.Method: break; } return false; } void makeNotNullMembersMaybeNull() { if (_symbol is MethodSymbol method) { if (method.IsConstructor()) { if (needsDefaultInitialStateForMembers()) { foreach (var member in method.ContainingType.GetMembersUnordered()) { if (member.IsStatic != method.IsStatic) { continue; } var memberToInitialize = member; switch (member) { case PropertySymbol: // skip any manually implemented properties. continue; case FieldSymbol { IsConst: true }: continue; case FieldSymbol { AssociatedSymbol: PropertySymbol prop }: // We want to initialize auto-property state to the default state, but not computed properties. memberToInitialize = prop; break; default: break; } var memberSlot = getSlotForFieldOrPropertyOrEvent(memberToInitialize); if (memberSlot > 0) { var type = memberToInitialize.GetTypeOrReturnType(); if (!type.NullableAnnotation.IsOblivious()) { this.State[memberSlot] = type.Type.IsPossiblyNullableReferenceTypeTypeParameter() ? NullableFlowState.MaybeDefault : NullableFlowState.MaybeNull; } } } } } else { do { makeMembersMaybeNull(method, method.NotNullMembers); makeMembersMaybeNull(method, method.NotNullWhenTrueMembers); makeMembersMaybeNull(method, method.NotNullWhenFalseMembers); method = method.OverriddenMethod; } while (method != null); } } bool needsDefaultInitialStateForMembers() { if (_hasInitialState) { return false; } if (!method.HasThisConstructorInitializer() && (!method.ContainingType.IsValueType || method.IsStatic)) { return true; } return methodMainNode is BoundConstructorMethodBody ctorBody && ctorBody.Initializer?.Expression.ExpressionSymbol is MethodSymbol delegatedCtor && delegatedCtor.IsDefaultValueTypeConstructor(); } } void makeMembersMaybeNull(MethodSymbol method, ImmutableArray members) { foreach (var memberName in members) { makeMemberMaybeNull(method, memberName); } } void makeMemberMaybeNull(MethodSymbol method, string memberName) { var type = method.ContainingType; foreach (var member in type.GetMembers(memberName)) { if (getSlotForFieldOrPropertyOrEvent(member) is int memberSlot && memberSlot > 0) { this.State[memberSlot] = NullableFlowState.MaybeNull; } } } void enforceNotNullWhenForPendingReturn(PendingBranch pendingReturn, BoundReturnStatement returnStatement) { var parameters = this.MethodParameters; if (!parameters.IsEmpty) { if (pendingReturn.IsConditionalState) { enforceParameterNotNullWhen(returnStatement.Syntax, parameters, sense: true, stateWhen: pendingReturn.StateWhenTrue); enforceParameterNotNullWhen(returnStatement.Syntax, parameters, sense: false, stateWhen: pendingReturn.StateWhenFalse); } } } void enforceNotNull(SyntaxNode? syntaxOpt, LocalState state) { if (!state.Reachable) { return; } foreach (var parameter in this.MethodParameters) { var slot = GetOrCreateSlot(parameter); if (slot <= 0) { continue; } var annotations = parameter.FlowAnalysisAnnotations; var hasNotNull = (annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull; var parameterState = state[slot]; if (hasNotNull && parameterState.MayBeNull()) { // Parameter '{name}' must have a non-null value when exiting. Diagnostics.Add(ErrorCode.WRN_ParameterDisallowsNull, syntaxOpt?.GetLocation() ?? methodMainNode.Syntax.GetLastToken().GetLocation(), parameter.Name); } else { EnforceNotNullIfNotNull(syntaxOpt, state, this.MethodParameters, parameter.NotNullIfParameterNotNull, parameterState, parameter); } } } void enforceParameterNotNullWhen(SyntaxNode syntax, ImmutableArray parameters, bool sense, LocalState stateWhen) { if (!stateWhen.Reachable) { return; } foreach (var parameter in parameters) { if (parameterHasBadConditionalState(parameter, sense, stateWhen)) { // Parameter '{name}' must have a non-null value when exiting with '{sense}'. Diagnostics.Add(ErrorCode.WRN_ParameterConditionallyDisallowsNull, syntax.Location, parameter.Name, sense ? "true" : "false"); } } } bool parameterHasBadConditionalState(ParameterSymbol parameter, bool sense, LocalState stateWhen) { var refKind = parameter.RefKind; if (refKind != RefKind.Out && refKind != RefKind.Ref) { return false; } var slot = GetOrCreateSlot(parameter); if (slot > 0) { var parameterState = stateWhen[slot]; // On a parameter marked with MaybeNullWhen, we would have not reported an assignment warning. // We should only check if an assignment warning would have been warranted ignoring the MaybeNullWhen. FlowAnalysisAnnotations annotations = parameter.FlowAnalysisAnnotations; if (sense) { bool hasNotNullWhenTrue = (annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNullWhenTrue; bool hasMaybeNullWhenFalse = (annotations & FlowAnalysisAnnotations.MaybeNull) == FlowAnalysisAnnotations.MaybeNullWhenFalse; return (hasNotNullWhenTrue && parameterState.MayBeNull()) || (hasMaybeNullWhenFalse && ShouldReportNullableAssignment(parameter.TypeWithAnnotations, parameterState)); } else { bool hasNotNullWhenFalse = (annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNullWhenFalse; bool hasMaybeNullWhenTrue = (annotations & FlowAnalysisAnnotations.MaybeNull) == FlowAnalysisAnnotations.MaybeNullWhenTrue; return (hasNotNullWhenFalse && parameterState.MayBeNull()) || (hasMaybeNullWhenTrue && ShouldReportNullableAssignment(parameter.TypeWithAnnotations, parameterState)); } } return false; } int getSlotForFieldOrPropertyOrEvent(Symbol member) { if (member.Kind != SymbolKind.Field && member.Kind != SymbolKind.Property && member.Kind != SymbolKind.Event) { return -1; } int containingSlot = 0; if (!member.IsStatic) { if (MethodThisParameter is null) { return -1; } containingSlot = GetOrCreateSlot(MethodThisParameter); if (containingSlot < 0) { return -1; } Debug.Assert(containingSlot > 0); } return GetOrCreateSlot(member, containingSlot); } } private void EnforceNotNullIfNotNull(SyntaxNode? syntaxOpt, LocalState state, ImmutableArray parameters, ImmutableHashSet inputParamNames, NullableFlowState outputState, ParameterSymbol? outputParam) { if (inputParamNames.IsEmpty || outputState.IsNotNull()) { return; } foreach (var inputParam in parameters) { if (inputParamNames.Contains(inputParam.Name) && GetOrCreateSlot(inputParam) is > 0 and int inputSlot && state[inputSlot].IsNotNull()) { var location = syntaxOpt?.GetLocation() ?? methodMainNode.Syntax.GetLastToken().GetLocation(); if (outputParam is object) { // Parameter '{0}' must have a non-null value when exiting because parameter '{1}' is non-null. Diagnostics.Add(ErrorCode.WRN_ParameterNotNullIfNotNull, location, outputParam.Name, inputParam.Name); } else { // Return value must be non-null because parameter '{0}' is non-null. Diagnostics.Add(ErrorCode.WRN_ReturnNotNullIfNotNull, location, inputParam.Name); } break; } } } private void EnforceDoesNotReturn(SyntaxNode? syntaxOpt) { // DoesNotReturn is only supported in member methods if (CurrentSymbol is MethodSymbol { ContainingSymbol: TypeSymbol _ } method && ((method.FlowAnalysisAnnotations & FlowAnalysisAnnotations.DoesNotReturn) == FlowAnalysisAnnotations.DoesNotReturn) && this.IsReachable()) { // A method marked [DoesNotReturn] should not return. ReportDiagnostic(ErrorCode.WRN_ShouldNotReturn, syntaxOpt?.GetLocation() ?? methodMainNode.Syntax.GetLastToken().GetLocation()); } } /// /// Analyzes a method body if settings indicate we should, discarding the final nullable state. /// internal static void AnalyzeIfNeeded( CSharpCompilation compilation, MethodSymbol method, BoundNode node, DiagnosticBag diagnostics, bool useConstructorExitWarnings, VariableState? initialNullableState) { AnalyzeIfNeeded(compilation, method, node, diagnostics, useConstructorExitWarnings, initialNullableState, getFinalNullableState: false, out _); } internal static void AnalyzeIfNeeded( CSharpCompilation compilation, MethodSymbol method, BoundNode node, DiagnosticBag diagnostics, bool useConstructorExitWarnings, VariableState? initialNullableState, bool getFinalNullableState, out VariableState? finalNullableState) { if (compilation.LanguageVersion < MessageID.IDS_FeatureNullableReferenceTypes.RequiredVersion() || !compilation.ShouldRunNullableWalker) { #if DEBUG // Always run analysis in debug builds so that we can more reliably catch // nullable regressions e.g. https://github.com/dotnet/roslyn/issues/40136 // Once we address https://github.com/dotnet/roslyn/issues/46579 we should also always pass `getFinalNullableState: true` in debug mode. // We will likely always need to write a 'null' out for the out parameter in this code path, though, because // we don't want to introduce behavior differences between debug and release builds Analyze(compilation, method, node, new DiagnosticBag(), useConstructorExitWarnings: false, initialNullableState: null, getFinalNullableState: false, out _); #endif finalNullableState = null; return; } Analyze(compilation, method, node, diagnostics, useConstructorExitWarnings, initialNullableState, getFinalNullableState, out finalNullableState); } internal static void Analyze( CSharpCompilation compilation, MethodSymbol method, BoundNode node, DiagnosticBag diagnostics, bool useConstructorExitWarnings, VariableState? initialNullableState, bool getFinalNullableState, out VariableState? finalNullableState) { if (method.IsImplicitlyDeclared && !method.IsImplicitConstructor && !method.IsScriptInitializer) { finalNullableState = null; return; } Debug.Assert(node.SyntaxTree is object); var binder = method is SynthesizedSimpleProgramEntryPointSymbol entryPoint ? entryPoint.GetBodyBinder(ignoreAccessibility: false) : compilation.GetBinderFactory(node.SyntaxTree).GetBinder(node.Syntax); var conversions = binder.Conversions; Analyze(compilation, method, node, binder, conversions, diagnostics, useConstructorExitWarnings, useDelegateInvokeParameterTypes: false, delegateInvokeMethodOpt: null, initialState: initialNullableState, analyzedNullabilityMapOpt: null, snapshotBuilderOpt: null, returnTypesOpt: null, getFinalNullableState, finalNullableState: out finalNullableState); } /// /// Gets the "after initializers state" which should be used at the beginning of nullable analysis /// of certain constructors. Only used for semantic model and debug verification. /// internal static VariableState? GetAfterInitializersState(CSharpCompilation compilation, Symbol? symbol) { if (symbol is MethodSymbol method && method.IncludeFieldInitializersInBody() && method.ContainingType is SourceMemberContainerTypeSymbol containingType) { var unusedDiagnostics = DiagnosticBag.GetInstance(); Binder.ProcessedFieldInitializers initializers = default; Binder.BindFieldInitializers(compilation, null, method.IsStatic ? containingType.StaticInitializers : containingType.InstanceInitializers, unusedDiagnostics, ref initializers); NullableWalker.AnalyzeIfNeeded( compilation, method, InitializerRewriter.RewriteConstructor(initializers.BoundInitializers, method), unusedDiagnostics, useConstructorExitWarnings: false, initialNullableState: null, getFinalNullableState: true, out var afterInitializersState); unusedDiagnostics.Free(); return afterInitializersState; } return null; } #if DEBUG /// /// Analyzes a set of bound nodes, recording updated nullability information. This method is only /// used during debug runs when nullability is disabled to verify that correct semantic information /// is being recorded for all bound nodes. The results are thrown away. /// internal static void AnalyzeWithoutRewrite( CSharpCompilation compilation, Symbol symbol, BoundNode node, Binder binder, DiagnosticBag diagnostics, bool createSnapshots) { _ = AnalyzeWithSemanticInfo(compilation, symbol, node, binder, initialState: GetAfterInitializersState(compilation, symbol), diagnostics, createSnapshots); } #endif /// /// Analyzes a set of bound nodes, recording updated nullability information, and returns an /// updated BoundNode with the information populated.. /// internal static BoundNode AnalyzeAndRewrite( CSharpCompilation compilation, Symbol symbol, BoundNode node, Binder binder, VariableState? initialState, DiagnosticBag diagnostics, bool createSnapshots, out SnapshotManager? snapshotManager, ref ImmutableDictionary? remappedSymbols) { ImmutableDictionary analyzedNullabilitiesMap; (snapshotManager, analyzedNullabilitiesMap) = AnalyzeWithSemanticInfo(compilation, symbol, node, binder, initialState, diagnostics, createSnapshots); return Rewrite(analyzedNullabilitiesMap, snapshotManager, node, ref remappedSymbols); } private static (SnapshotManager?, ImmutableDictionary) AnalyzeWithSemanticInfo( CSharpCompilation compilation, Symbol symbol, BoundNode node, Binder binder, VariableState? initialState, DiagnosticBag diagnostics, bool createSnapshots) { var analyzedNullabilities = ImmutableDictionary.CreateBuilder(EqualityComparer.Default, NullabilityInfoTypeComparer.Instance); // Attributes don't have a symbol, which is what SnapshotBuilder uses as an index for maintaining global state. // Until we have a workaround for this, disable snapshots for null symbols. // https://github.com/dotnet/roslyn/issues/36066 var snapshotBuilder = createSnapshots && symbol != null ? new SnapshotManager.Builder() : null; Analyze( compilation, symbol, node, binder, binder.Conversions, diagnostics, useConstructorExitWarnings: true, useDelegateInvokeParameterTypes: false, delegateInvokeMethodOpt: null, initialState, analyzedNullabilities, snapshotBuilder, returnTypesOpt: null, getFinalNullableState: false, out _); var analyzedNullabilitiesMap = analyzedNullabilities.ToImmutable(); var snapshotManager = snapshotBuilder?.ToManagerAndFree(); #if DEBUG // https://github.com/dotnet/roslyn/issues/34993 Enable for all calls if (compilation.NullableSemanticAnalysisEnabled) { DebugVerifier.Verify(analyzedNullabilitiesMap, snapshotManager, node); } #endif return (snapshotManager, analyzedNullabilitiesMap); } internal static BoundNode AnalyzeAndRewriteSpeculation( int position, BoundNode node, Binder binder, SnapshotManager originalSnapshots, out SnapshotManager newSnapshots, ref ImmutableDictionary? remappedSymbols) { var analyzedNullabilities = ImmutableDictionary.CreateBuilder(EqualityComparer.Default, NullabilityInfoTypeComparer.Instance); var newSnapshotBuilder = new SnapshotManager.Builder(); var (walker, initialState, symbol) = originalSnapshots.RestoreWalkerToAnalyzeNewNode(position, node, binder, analyzedNullabilities, newSnapshotBuilder); try { Analyze(walker, symbol, diagnostics: null, initialState, snapshotBuilderOpt: newSnapshotBuilder); } finally { walker.Free(); } var analyzedNullabilitiesMap = analyzedNullabilities.ToImmutable(); newSnapshots = newSnapshotBuilder.ToManagerAndFree(); #if DEBUG if (binder.Compilation.NullableSemanticAnalysisEnabled) { DebugVerifier.Verify(analyzedNullabilitiesMap, newSnapshots, node); } #endif return Rewrite(analyzedNullabilitiesMap, newSnapshots, node, ref remappedSymbols); } private static BoundNode Rewrite(ImmutableDictionary updatedNullabilities, SnapshotManager? snapshotManager, BoundNode node, ref ImmutableDictionary? remappedSymbols) { var remappedSymbolsBuilder = ImmutableDictionary.CreateBuilder(Symbols.SymbolEqualityComparer.ConsiderEverything, Symbols.SymbolEqualityComparer.ConsiderEverything); if (remappedSymbols is object) { // When we're rewriting for the speculative model, there will be a set of originally-mapped symbols, and we need to // use them in addition to any symbols found during this pass of the walker. remappedSymbolsBuilder.AddRange(remappedSymbols); } var rewriter = new NullabilityRewriter(updatedNullabilities, snapshotManager, remappedSymbolsBuilder); var rewrittenNode = rewriter.Visit(node); remappedSymbols = remappedSymbolsBuilder.ToImmutable(); return rewrittenNode; } internal static void AnalyzeIfNeeded( Binder binder, BoundAttribute attribute, DiagnosticBag diagnostics) { var compilation = binder.Compilation; if (compilation.LanguageVersion < MessageID.IDS_FeatureNullableReferenceTypes.RequiredVersion() || !compilation.ShouldRunNullableWalker) { #if DEBUG // Always run analysis in debug builds so that we can more reliably catch // nullable regressions e.g. https://github.com/dotnet/roslyn/issues/40136 diagnostics = new DiagnosticBag(); #else return; #endif } Analyze( compilation, symbol: null, attribute, binder, binder.Conversions, diagnostics, useConstructorExitWarnings: false, useDelegateInvokeParameterTypes: false, delegateInvokeMethodOpt: null, initialState: null, analyzedNullabilityMapOpt: null, snapshotBuilderOpt: null, returnTypesOpt: null, getFinalNullableState: false, out _); } internal static void Analyze( CSharpCompilation compilation, BoundLambda lambda, Conversions conversions, DiagnosticBag diagnostics, MethodSymbol? delegateInvokeMethodOpt, VariableState? initialState, ImmutableDictionary.Builder? analyzedNullabilityMapOpt, SnapshotManager.Builder? snapshotBuilderOpt, ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)>? returnTypesOpt) { Analyze( compilation, lambda.Symbol, lambda.Body, lambda.Binder, conversions, diagnostics, useConstructorExitWarnings: false, useDelegateInvokeParameterTypes: UseDelegateInvokeParameterTypes(lambda, delegateInvokeMethodOpt), delegateInvokeMethodOpt: delegateInvokeMethodOpt, initialState, analyzedNullabilityMapOpt, snapshotBuilderOpt, returnTypesOpt, getFinalNullableState: false, out _); } private static void Analyze( CSharpCompilation compilation, Symbol? symbol, BoundNode node, Binder binder, Conversions conversions, DiagnosticBag diagnostics, bool useConstructorExitWarnings, bool useDelegateInvokeParameterTypes, MethodSymbol? delegateInvokeMethodOpt, VariableState? initialState, ImmutableDictionary.Builder? analyzedNullabilityMapOpt, SnapshotManager.Builder? snapshotBuilderOpt, ArrayBuilder<(BoundReturnStatement, TypeWithAnnotations)>? returnTypesOpt, bool getFinalNullableState, out VariableState? finalNullableState) { Debug.Assert(diagnostics != null); var walker = new NullableWalker(compilation, symbol, useConstructorExitWarnings, useDelegateInvokeParameterTypes, delegateInvokeMethodOpt, node, binder, conversions, initialState, returnTypesOpt, analyzedNullabilityMapOpt, snapshotBuilderOpt); finalNullableState = null; try { Analyze(walker, symbol, diagnostics, initialState, snapshotBuilderOpt); if (getFinalNullableState) { Debug.Assert(!walker.IsConditionalState); finalNullableState = walker.GetVariableState(walker.State); } } finally { walker.Free(); } } private static void Analyze( NullableWalker walker, Symbol? symbol, DiagnosticBag? diagnostics, VariableState? initialState, SnapshotManager.Builder? snapshotBuilderOpt) { Debug.Assert(snapshotBuilderOpt is null || symbol is object); try { bool badRegion = false; Optional initialLocalState = initialState is null ? default : new Optional(initialState.VariableNullableStates); var previousSlot = snapshotBuilderOpt?.EnterNewWalker(symbol!) ?? -1; ImmutableArray returns = walker.Analyze(ref badRegion, initialLocalState); snapshotBuilderOpt?.ExitWalker(walker.SaveSharedState(), previousSlot); diagnostics?.AddRange(walker.Diagnostics); Debug.Assert(!badRegion); } catch (CancelledByStackGuardException ex) when (diagnostics != null) { ex.AddAnError(diagnostics); } } private SharedWalkerState SaveSharedState() => new SharedWalkerState( _variableSlot.ToImmutableDictionary(), ImmutableArray.Create(variableBySlot, start: 0, length: nextVariableSlot), _variableTypes.ToImmutableDictionary(), CurrentSymbol); private void TakeIncrementalSnapshot(BoundNode? node) { Debug.Assert(!IsConditionalState); _snapshotBuilderOpt?.TakeIncrementalSnapshot(node, State); } private void SetUpdatedSymbol(BoundNode node, Symbol originalSymbol, Symbol updatedSymbol) { if (_snapshotBuilderOpt is null) { return; } var lambdaIsExactMatch = false; if (node is BoundLambda boundLambda && originalSymbol is LambdaSymbol l && updatedSymbol is NamedTypeSymbol n) { if (!AreLambdaAndNewDelegateSimilar(l, n)) { return; } lambdaIsExactMatch = updatedSymbol.Equals(boundLambda.Type!.GetDelegateType(), TypeCompareKind.ConsiderEverything); } #if DEBUG Debug.Assert(node is object); Debug.Assert(AreCloseEnough(originalSymbol, updatedSymbol), $"Attempting to set {node.Syntax} from {originalSymbol.ToDisplayString()} to {updatedSymbol.ToDisplayString()}"); #endif if (lambdaIsExactMatch || Symbol.Equals(originalSymbol, updatedSymbol, TypeCompareKind.ConsiderEverything)) { // If the symbol is reset, remove the updated symbol so we don't needlessly update the // bound node later on. We do this unconditionally, as Remove will just return false // if the key wasn't in the dictionary. _snapshotBuilderOpt.RemoveSymbolIfPresent(node, originalSymbol); } else { _snapshotBuilderOpt.SetUpdatedSymbol(node, originalSymbol, updatedSymbol); } } 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++) { PopulateOneSlot(ref state, slot); } } private void PopulateOneSlot(ref LocalState state, int slot) { state[slot] = GetDefaultState(ref state, slot); } private NullableFlowState GetDefaultState(ref LocalState state, int slot) { Debug.Assert(slot > 0); if (!state.Reachable) return NullableFlowState.NotNull; var variable = variableBySlot[slot]; var symbol = variable.Symbol; switch (symbol.Kind) { case SymbolKind.Local: { var local = (LocalSymbol)symbol; if (!_variableTypes.TryGetValue(local, out TypeWithAnnotations localType)) { localType = local.TypeWithAnnotations; } return localType.ToTypeWithState().State; } case SymbolKind.Parameter: { var parameter = (ParameterSymbol)symbol; if (!_variableTypes.TryGetValue(parameter, out TypeWithAnnotations parameterType)) { parameterType = parameter.TypeWithAnnotations; } return GetParameterState(parameterType, parameter.FlowAnalysisAnnotations).State; } case SymbolKind.Field: case SymbolKind.Property: case SymbolKind.Event: return GetDefaultState(symbol); case SymbolKind.ErrorType: return NullableFlowState.NotNull; default: throw ExceptionUtilities.UnexpectedValue(symbol.Kind); } } protected override bool TryGetReceiverAndMember(BoundExpression expr, out BoundExpression? receiver, [NotNullWhen(true)] out Symbol? member) { receiver = null; member = null; switch (expr.Kind) { case BoundKind.FieldAccess: { var fieldAccess = (BoundFieldAccess)expr; var fieldSymbol = fieldAccess.FieldSymbol; member = fieldSymbol; if (fieldSymbol.IsFixedSizeBuffer) { return false; } if (fieldSymbol.IsStatic) { return true; } receiver = fieldAccess.ReceiverOpt; break; } case BoundKind.EventAccess: { var eventAccess = (BoundEventAccess)expr; var eventSymbol = eventAccess.EventSymbol; // https://github.com/dotnet/roslyn/issues/29901 Use AssociatedField for field-like events? member = eventSymbol; if (eventSymbol.IsStatic) { return true; } receiver = eventAccess.ReceiverOpt; break; } case BoundKind.PropertyAccess: { var propAccess = (BoundPropertyAccess)expr; var propSymbol = propAccess.PropertySymbol; member = propSymbol; if (propSymbol.IsStatic) { return true; } receiver = propAccess.ReceiverOpt; break; } } Debug.Assert(member?.RequiresInstanceReceiver() ?? true); return member is object && receiver is object && receiver.Kind != BoundKind.TypeExpression && receiver.Type is object; } protected override int MakeSlot(BoundExpression node) { int result = makeSlot(node); #if DEBUG if (result != -1) { // Check that the slot represents a value of an equivalent type to the node TypeSymbol slotType = NominalSlotType(result); TypeSymbol? nodeType = node.Type; HashSet? discardedUseSiteDiagnostics = null; var conversionsWithoutNullability = this.compilation.Conversions; Debug.Assert(node.HasErrors || nodeType!.IsErrorType() || conversionsWithoutNullability.HasIdentityOrImplicitReferenceConversion(slotType, nodeType, ref discardedUseSiteDiagnostics) || conversionsWithoutNullability.HasBoxingConversion(slotType, nodeType, ref discardedUseSiteDiagnostics)); } #endif return result; int makeSlot(BoundExpression node) { switch (node.Kind) { case BoundKind.ThisReference: case BoundKind.BaseReference: { var method = getTopLevelMethod(_symbol as MethodSymbol); var thisParameter = method?.ThisParameter; return thisParameter is object ? GetOrCreateSlot(thisParameter) : -1; } case BoundKind.Conversion: { int slot = getPlaceholderSlot(node); if (slot > 0) { return slot; } var conv = (BoundConversion)node; switch (conv.Conversion.Kind) { case ConversionKind.ExplicitNullable: { var operand = conv.Operand; var operandType = operand.Type; var convertedType = conv.Type; if (AreNullableAndUnderlyingTypes(operandType, convertedType, out _)) { // Explicit conversion of Nullable 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 _); } } break; case ConversionKind.Identity: case ConversionKind.DefaultLiteral: case ConversionKind.ImplicitReference: case ConversionKind.ImplicitTupleLiteral: case ConversionKind.Boxing: // No need to create a slot for the boxed value (in the Boxing case) since assignment already // clones slots and there is not another scenario where creating a slot is observable. return MakeSlot(conv.Operand); } } break; case BoundKind.DefaultLiteral: case BoundKind.DefaultExpression: case BoundKind.ObjectCreationExpression: case BoundKind.DynamicObjectCreationExpression: case BoundKind.AnonymousObjectCreationExpression: case BoundKind.NewT: case BoundKind.TupleLiteral: case BoundKind.ConvertedTupleLiteral: return getPlaceholderSlot(node); case BoundKind.ConditionalAccess: return getPlaceholderSlot(node); case BoundKind.ConditionalReceiver: return _lastConditionalAccessSlot; default: { int slot = getPlaceholderSlot(node); return (slot > 0) ? slot : base.MakeSlot(node); } } return -1; } int getPlaceholderSlot(BoundExpression expr) { if (_placeholderLocalsOpt != null && _placeholderLocalsOpt.TryGetValue(expr, out var placeholder)) { return GetOrCreateSlot(placeholder); } return -1; } static MethodSymbol? getTopLevelMethod(MethodSymbol? method) { while (method is object) { var container = method.ContainingSymbol; if (container.Kind == SymbolKind.NamedType) { return method; } method = container as MethodSymbol; } return null; } } protected override int GetOrCreateSlot(Symbol symbol, int containingSlot = 0, bool forceSlotEvenIfEmpty = false) { if (containingSlot > 0 && !IsSlotMember(containingSlot, symbol)) return -1; return base.GetOrCreateSlot(symbol, containingSlot, forceSlotEvenIfEmpty); } private void VisitAndUnsplitAll(ImmutableArray nodes) where T : BoundNode { if (nodes.IsDefault) { return; } foreach (var node in nodes) { Visit(node); Unsplit(); } } private void VisitWithoutDiagnostics(BoundNode? node) { var previousDiagnostics = _disableDiagnostics; _disableDiagnostics = true; Visit(node); _disableDiagnostics = previousDiagnostics; } protected override void VisitRvalue(BoundExpression? node, bool isKnownToBeAnLvalue = false) { Visit(node); VisitRvalueEpilogue(node); } /// /// The contents of this method, particularly , are problematic when /// inlined. The methods themselves are small but they end up allocating significantly larger /// frames due to the use of biggish value types within them. The method /// is used on a hot path for fluent calls and this size change is enough that it causes us /// to exceed our thresholds in EndToEndTests.OverflowOnFluentCall. /// [MethodImpl(MethodImplOptions.NoInlining)] private void VisitRvalueEpilogue(BoundExpression? node) { Unsplit(); UseRvalueOnly(node); // drop lvalue part } private TypeWithState VisitRvalueWithState(BoundExpression? node) { VisitRvalue(node); return ResultType; } private TypeWithAnnotations VisitLvalueWithAnnotations(BoundExpression node) { VisitLValue(node); Unsplit(); return LvalueResultType; } private static object GetTypeAsDiagnosticArgument(TypeSymbol? typeOpt) { return typeOpt ?? (object)""; } private static object GetParameterAsDiagnosticArgument(ParameterSymbol? parameterOpt) { return parameterOpt is null ? (object)"" : new FormattedSymbol(parameterOpt, SymbolDisplayFormat.ShortFormat); } private static object GetContainingSymbolAsDiagnosticArgument(ParameterSymbol? parameterOpt) { var containingSymbol = parameterOpt?.ContainingSymbol; return containingSymbol is null ? (object)"" : new FormattedSymbol(containingSymbol, SymbolDisplayFormat.MinimallyQualifiedFormat); } private enum AssignmentKind { Assignment, Return, Argument, ForEachIterationVariable } /// /// Should we warn for assigning this state into this type? /// /// This should often be checked together with /// It catches putting a `null` into a `[DisallowNull]int?` for example, which cannot simply be represented as a non-nullable target type. /// private static bool ShouldReportNullableAssignment(TypeWithAnnotations type, NullableFlowState state) { if (!type.HasType || type.Type.IsValueType) { return false; } switch (type.NullableAnnotation) { case NullableAnnotation.Oblivious: case NullableAnnotation.Annotated: return false; } switch (state) { case NullableFlowState.NotNull: return false; case NullableFlowState.MaybeNull: // https://github.com/dotnet/roslyn/issues/46044: Skip the following check if /langversion > 8? if (type.Type.IsTypeParameterDisallowingAnnotationInCSharp8() && !(type.Type is TypeParameterSymbol { IsNotNullable: true })) { return false; } break; } return true; } /// /// Reports top-level nullability problem in assignment. /// Any conversion of the value should have been applied. /// private void ReportNullableAssignmentIfNecessary( BoundExpression? value, TypeWithAnnotations targetType, TypeWithState valueType, bool useLegacyWarnings, AssignmentKind assignmentKind = AssignmentKind.Assignment, ParameterSymbol? parameterOpt = null, Location? location = null) { // Callers should apply any conversions before calling this method // (see https://github.com/dotnet/roslyn/issues/39867). if (targetType.HasType && !targetType.Type.Equals(valueType.Type, TypeCompareKind.AllIgnoreOptions)) { return; } if (value == null || // This prevents us from giving undesired warnings for synthesized arguments for optional parameters, // but allows us to give warnings for synthesized assignments to record properties and for parameter default values at the declaration site. (value.WasCompilerGenerated && assignmentKind == AssignmentKind.Argument) || !ShouldReportNullableAssignment(targetType, valueType.State)) { return; } location ??= value.Syntax.GetLocation(); var unwrappedValue = SkipReferenceConversions(value); if (unwrappedValue.IsSuppressed) { return; } if (value.ConstantValue?.IsNull == true && !useLegacyWarnings) { // 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)`). ReportDiagnostic(assignmentKind == AssignmentKind.Return ? ErrorCode.WRN_NullReferenceReturn : ErrorCode.WRN_NullAsNonNullable, location); } else if (assignmentKind == AssignmentKind.Argument) { ReportDiagnostic(ErrorCode.WRN_NullReferenceArgument, location, GetParameterAsDiagnosticArgument(parameterOpt), GetContainingSymbolAsDiagnosticArgument(parameterOpt)); } else if (useLegacyWarnings) { // https://github.com/dotnet/roslyn/issues/46044: Skip the following check if /langversion > 8? if (isMaybeDefaultValue(valueType)) { // No W warning reported assigning or casting [MaybeNull]T value to T // because there is no syntax for declaring the target type as [MaybeNull]T. return; } ReportNonSafetyDiagnostic(location); } else { ReportDiagnostic(assignmentKind == AssignmentKind.Return ? ErrorCode.WRN_NullReferenceReturn : ErrorCode.WRN_NullReferenceAssignment, location); } static bool isMaybeDefaultValue(TypeWithState valueType) { return valueType.Type?.TypeKind == TypeKind.TypeParameter && valueType.State == NullableFlowState.MaybeDefault; } } internal static bool AreParameterAnnotationsCompatible( RefKind refKind, TypeWithAnnotations overriddenType, FlowAnalysisAnnotations overriddenAnnotations, TypeWithAnnotations overridingType, FlowAnalysisAnnotations overridingAnnotations, bool forRef = false) { // We've already checked types and annotations, let's check nullability attributes as well // Return value is treated as an `out` parameter (or `ref` if it is a `ref` return) if (refKind == RefKind.Ref) { // ref variables are invariant return AreParameterAnnotationsCompatible(RefKind.None, overriddenType, overriddenAnnotations, overridingType, overridingAnnotations, forRef: true) && AreParameterAnnotationsCompatible(RefKind.Out, overriddenType, overriddenAnnotations, overridingType, overridingAnnotations); } if (refKind == RefKind.None || refKind == RefKind.In) { // pre-condition attributes // Check whether we can assign a value from overridden parameter to overriding var valueState = GetParameterState(overriddenType, overriddenAnnotations); if (isBadAssignment(valueState, overridingType, overridingAnnotations)) { return false; } // unconditional post-condition attributes on inputs bool overridingHasNotNull = (overridingAnnotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull; bool overriddenHasNotNull = (overriddenAnnotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull; if (overriddenHasNotNull && !overridingHasNotNull && !forRef) { // Overriding doesn't conform to contract of overridden (ie. promise not to return if parameter is null) return false; } bool overridingHasMaybeNull = (overridingAnnotations & FlowAnalysisAnnotations.MaybeNull) == FlowAnalysisAnnotations.MaybeNull; bool overriddenHasMaybeNull = (overriddenAnnotations & FlowAnalysisAnnotations.MaybeNull) == FlowAnalysisAnnotations.MaybeNull; if (overriddenHasMaybeNull && !overridingHasMaybeNull && !forRef) { // Overriding doesn't conform to contract of overridden (ie. promise to only return if parameter is null) return false; } } if (refKind == RefKind.Out) { // post-condition attributes (`Maybe/NotNull` and `Maybe/NotNullWhen`) if (!canAssignOutputValueWhen(true) || !canAssignOutputValueWhen(false)) { return false; } } return true; bool canAssignOutputValueWhen(bool sense) { var valueWhen = ApplyUnconditionalAnnotations( overridingType.ToTypeWithState(), makeUnconditionalAnnotation(overridingAnnotations, sense)); var destAnnotationsWhen = ToInwardAnnotations(makeUnconditionalAnnotation(overriddenAnnotations, sense)); if (isBadAssignment(valueWhen, overriddenType, destAnnotationsWhen)) { // Can't assign value from overriding to overridden in 'sense' case return false; } return true; } static bool isBadAssignment(TypeWithState valueState, TypeWithAnnotations destinationType, FlowAnalysisAnnotations destinationAnnotations) { if (ShouldReportNullableAssignment( ApplyLValueAnnotations(destinationType, destinationAnnotations), valueState.State)) { return true; } if (IsDisallowedNullAssignment(valueState, destinationAnnotations)) { return true; } return false; } // Convert both conditional annotations to unconditional ones or nothing static FlowAnalysisAnnotations makeUnconditionalAnnotation(FlowAnalysisAnnotations annotations, bool sense) { if (sense) { var unconditionalAnnotationWhenTrue = makeUnconditionalAnnotationCore(annotations, FlowAnalysisAnnotations.NotNullWhenTrue, FlowAnalysisAnnotations.NotNull); return makeUnconditionalAnnotationCore(unconditionalAnnotationWhenTrue, FlowAnalysisAnnotations.MaybeNullWhenTrue, FlowAnalysisAnnotations.MaybeNull); } var unconditionalAnnotationWhenFalse = makeUnconditionalAnnotationCore(annotations, FlowAnalysisAnnotations.NotNullWhenFalse, FlowAnalysisAnnotations.NotNull); return makeUnconditionalAnnotationCore(unconditionalAnnotationWhenFalse, FlowAnalysisAnnotations.MaybeNullWhenFalse, FlowAnalysisAnnotations.MaybeNull); } // Convert Maybe/NotNullWhen into Maybe/NotNull or nothing static FlowAnalysisAnnotations makeUnconditionalAnnotationCore(FlowAnalysisAnnotations annotations, FlowAnalysisAnnotations conditionalAnnotation, FlowAnalysisAnnotations replacementAnnotation) { if ((annotations & conditionalAnnotation) != 0) { return annotations | replacementAnnotation; } return annotations & ~replacementAnnotation; } } private static bool IsDefaultValue(BoundExpression expr) { switch (expr.Kind) { case BoundKind.Conversion: { var conversion = (BoundConversion)expr; var conversionKind = conversion.Conversion.Kind; return (conversionKind == ConversionKind.DefaultLiteral || conversionKind == ConversionKind.NullLiteral) && IsDefaultValue(conversion.Operand); } case BoundKind.DefaultLiteral: case BoundKind.DefaultExpression: return true; default: return false; } } private void ReportNullabilityMismatchInAssignment(SyntaxNode syntaxNode, object sourceType, object destinationType) { ReportDiagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, syntaxNode, sourceType, destinationType); } private void ReportNullabilityMismatchInAssignment(Location location, object sourceType, object destinationType) { ReportDiagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, location, sourceType, destinationType); } /// /// Update tracked value on assignment. /// private void TrackNullableStateForAssignment( BoundExpression? valueOpt, TypeWithAnnotations targetType, int targetSlot, TypeWithState valueType, int valueSlot = -1) { Debug.Assert(!IsConditionalState); if (this.State.Reachable) { if (!targetType.HasType) { return; } if (targetSlot <= 0 || targetSlot == valueSlot) { return; } if (targetSlot >= this.State.Capacity) Normalize(ref this.State); var newState = valueType.State; SetStateAndTrackForFinally(ref this.State, targetSlot, newState); InheritDefaultState(targetType.Type, targetSlot); // https://github.com/dotnet/roslyn/issues/33428: Can the areEquivalentTypes check be removed // if InheritNullableStateOfMember asserts the member is valid for target and value? if (areEquivalentTypes(targetType, valueType)) { if (targetType.Type.IsReferenceType || targetType.TypeKind == TypeKind.TypeParameter || targetType.IsNullableType()) { if (valueSlot > 0) { InheritNullableStateOfTrackableType(targetSlot, valueSlot, skipSlot: targetSlot); } } else if (EmptyStructTypeCache.IsTrackableStructType(targetType.Type)) { InheritNullableStateOfTrackableStruct(targetType.Type, targetSlot, valueSlot, isDefaultValue: !(valueOpt is null) && IsDefaultValue(valueOpt), skipSlot: targetSlot); } } } static bool areEquivalentTypes(TypeWithAnnotations target, TypeWithState assignedValue) => target.Type.Equals(assignedValue.Type, TypeCompareKind.AllIgnoreOptions); } private void ReportNonSafetyDiagnostic(Location location) { ReportDiagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, location); } private void ReportDiagnostic(ErrorCode errorCode, SyntaxNode syntaxNode, params object[] arguments) { ReportDiagnostic(errorCode, syntaxNode.GetLocation(), arguments); } private void ReportDiagnostic(ErrorCode errorCode, Location location, params object[] arguments) { Debug.Assert(ErrorFacts.NullableWarnings.Contains(MessageProvider.Instance.GetIdForErrorCode((int)errorCode))); if (IsReachable() && !_disableDiagnostics) { Diagnostics.Add(errorCode, location, arguments); } } private void InheritNullableStateOfTrackableStruct(TypeSymbol targetType, int targetSlot, int valueSlot, bool isDefaultValue, int skipSlot = -1) { Debug.Assert(targetSlot > 0); Debug.Assert(EmptyStructTypeCache.IsTrackableStructType(targetType)); if (skipSlot < 0) { skipSlot = targetSlot; } if (!isDefaultValue && valueSlot > 0) { InheritNullableStateOfTrackableType(targetSlot, valueSlot, skipSlot); } else { foreach (var field in _emptyStructTypeCache.GetStructInstanceFields(targetType)) { InheritNullableStateOfMember(targetSlot, valueSlot, field, isDefaultValue: isDefaultValue, skipSlot); } } } private bool IsSlotMember(int slot, Symbol possibleMember) { TypeSymbol possibleBase = possibleMember.ContainingType; TypeSymbol possibleDerived = NominalSlotType(slot); HashSet? discardedUseSiteDiagnostics = null; var conversionsWithoutNullability = _conversions.WithNullability(false); return conversionsWithoutNullability.HasIdentityOrImplicitReferenceConversion(possibleDerived, possibleBase, ref discardedUseSiteDiagnostics) || conversionsWithoutNullability.HasBoxingConversion(possibleDerived, possibleBase, ref discardedUseSiteDiagnostics); } // '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); // Ensure member is valid for target and value. if (!IsSlotMember(targetContainerSlot, member)) return; TypeWithAnnotations fieldOrPropertyType = member.GetTypeOrReturnType(); if (fieldOrPropertyType.Type.IsReferenceType || fieldOrPropertyType.TypeKind == TypeKind.TypeParameter || fieldOrPropertyType.IsNullableType()) { int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot); if (targetMemberSlot > 0) { NullableFlowState value = isDefaultValue ? NullableFlowState.MaybeNull : fieldOrPropertyType.ToTypeWithState().State; int valueMemberSlot = -1; if (valueContainerSlot > 0) { valueMemberSlot = VariableSlot(member, valueContainerSlot); if (valueMemberSlot == skipSlot) { return; } value = valueMemberSlot > 0 && valueMemberSlot < this.State.Capacity ? this.State[valueMemberSlot] : NullableFlowState.NotNull; } SetStateAndTrackForFinally(ref this.State, targetMemberSlot, value); if (valueMemberSlot > 0) { InheritNullableStateOfTrackableType(targetMemberSlot, valueMemberSlot, skipSlot); } } } else if (EmptyStructTypeCache.IsTrackableStructType(fieldOrPropertyType.Type)) { int targetMemberSlot = GetOrCreateSlot(member, targetContainerSlot); if (targetMemberSlot > 0) { int valueMemberSlot = (valueContainerSlot > 0) ? GetOrCreateSlot(member, valueContainerSlot) : -1; if (valueMemberSlot == skipSlot) { return; } InheritNullableStateOfTrackableStruct(fieldOrPropertyType.Type, targetMemberSlot, valueMemberSlot, isDefaultValue: isDefaultValue, skipSlot); } } } private TypeSymbol NominalSlotType(int slot) { return variableBySlot[slot].Symbol.GetTypeOrReturnType().Type; } /// /// Whenever assigning a variable, and that variable is not declared at the point the state is being set, /// and the new state is not , this method should be called to perform the /// state setting and to ensure the mutation is visible outside the finally block when the mutation occurs in a /// finally block. /// private void SetStateAndTrackForFinally(ref LocalState state, int slot, NullableFlowState newState) { state[slot] = newState; if (newState != NullableFlowState.NotNull && NonMonotonicState.HasValue) { var tryState = NonMonotonicState.Value; tryState[slot] = newState.Join(tryState[slot]); NonMonotonicState = tryState; } } private void InheritDefaultState(TypeSymbol targetType, 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; var symbol = AsMemberOfType(targetType, variable.Symbol); SetStateAndTrackForFinally(ref this.State, slot, GetDefaultState(symbol)); InheritDefaultState(symbol.GetTypeOrReturnType().Type, slot); } } private NullableFlowState GetDefaultState(Symbol symbol) => ApplyUnconditionalAnnotations(symbol.GetTypeOrReturnType().ToTypeWithState(), GetRValueAnnotations(symbol)).State; 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); } } protected override LocalState TopState() { var state = LocalState.ReachableState(capacity: nextVariableSlot); Populate(ref state, start: 1); return state; } protected override LocalState UnreachableState() { return LocalState.UnreachableState; } protected override LocalState ReachableBottomState() { // Create a reachable state in which all variables are known to be non-null. return LocalState.ReachableState(capacity: nextVariableSlot); } private void EnterParameters() { if (!(CurrentSymbol is MethodSymbol methodSymbol)) { return; } if (methodSymbol is SynthesizedRecordConstructor) { if (_hasInitialState) { // A record primary constructor's parameters are entered before analyzing initializers. // On the second pass, the correct parameter states (potentially modified by initializers) // are contained in the initial state. return; } } else if (methodSymbol.IsConstructor()) { if (!_hasInitialState) { // For most constructors, we only enter parameters after analyzing initializers. return; } } // The partial definition part may include optional parameters whose default values we want to simulate assigning at the beginning of the method methodSymbol = methodSymbol.PartialDefinitionPart ?? methodSymbol; var methodParameters = methodSymbol.Parameters; var signatureParameters = (_useDelegateInvokeParameterTypes ? _delegateInvokeMethod! : methodSymbol).Parameters; // save a state representing the possibility that parameter default values were not assigned to the parameters. var parameterDefaultsNotAssignedState = State.Clone(); for (int i = 0; i < methodParameters.Length; i++) { var parameter = methodParameters[i]; // In error scenarios, the method can potentially have more parameters than the signature. If so, use the parameter type for those // errored parameters var parameterType = i >= signatureParameters.Length ? parameter.TypeWithAnnotations : signatureParameters[i].TypeWithAnnotations; EnterParameter(parameter, parameterType); } Join(ref State, ref parameterDefaultsNotAssignedState); } private void EnterParameter(ParameterSymbol parameter, TypeWithAnnotations parameterType) { _variableTypes[parameter] = parameterType; int slot = GetOrCreateSlot(parameter); Debug.Assert(!IsConditionalState); if (slot > 0) { var state = GetParameterState(parameterType, parameter.FlowAnalysisAnnotations).State; this.State[slot] = state; if (EmptyStructTypeCache.IsTrackableStructType(parameterType.Type)) { InheritNullableStateOfTrackableStruct( parameterType.Type, slot, valueSlot: -1, isDefaultValue: parameter.ExplicitDefaultConstantValue?.IsNull == true); } if (parameter.IsOptional) { // When a parameter has a default value, we initialize its flow state by simulating an assignment of the default value to the parameter. var syntax = parameter.GetNonNullSyntaxNode(); var rightSyntax = syntax is ParameterSyntax { Default: { Value: { } value } } ? value : syntax; var right = GetDefaultParameterValue(rightSyntax, parameter); var parameterAnnotations = GetParameterAnnotations(parameter); var parameterLValueType = ApplyLValueAnnotations(parameterType, parameterAnnotations); var previous = _disableNullabilityAnalysis; _disableNullabilityAnalysis = true; VisitOptionalImplicitConversion(right, parameterLValueType, useLegacyWarnings: true, trackMembers: true, assignmentKind: AssignmentKind.Assignment); _disableNullabilityAnalysis = previous; // If the LHS has annotations, we perform an additional check for nullable value types CheckDisallowedNullAssignment(ResultType, parameterAnnotations, right.Syntax.Location); TrackNullableStateForAssignment(right, parameterType, slot, ResultType, MakeSlot(right)); } } } private static TypeWithState GetParameterState(TypeWithAnnotations parameterType, FlowAnalysisAnnotations parameterAnnotations) { if ((parameterAnnotations & FlowAnalysisAnnotations.AllowNull) != 0) { return TypeWithState.Create(parameterType.Type, NullableFlowState.MaybeDefault); } if ((parameterAnnotations & FlowAnalysisAnnotations.DisallowNull) != 0) { return TypeWithState.Create(parameterType.Type, NullableFlowState.NotNull); } return parameterType.ToTypeWithState(); } public sealed override BoundNode? VisitReturnStatement(BoundReturnStatement node) { Debug.Assert(!IsConditionalState); var expr = node.ExpressionOpt; if (expr == null) { EnforceDoesNotReturn(node.Syntax); PendingBranches.Add(new PendingBranch(node, this.State, label: null)); SetUnreachable(); return null; } // Should not convert to method return type when inferring return type (when _returnTypesOpt != null). if (_returnTypesOpt == null && TryGetReturnType(out TypeWithAnnotations returnType, out FlowAnalysisAnnotations returnAnnotations)) { if (node.RefKind == RefKind.None && returnType.Type.SpecialType == SpecialType.System_Boolean) { // visit the expression without unsplitting, then check parameters marked with flow analysis attributes Visit(expr); } else { TypeWithState returnState; if (node.RefKind == RefKind.None) { returnState = VisitOptionalImplicitConversion(expr, returnType, useLegacyWarnings: false, trackMembers: false, AssignmentKind.Return); } else { // return ref expr; returnState = VisitRefExpression(expr, returnType); } // If the return has annotations, we perform an additional check for nullable value types CheckDisallowedNullAssignment(returnState, ToInwardAnnotations(returnAnnotations), node.Syntax.Location, boundValueOpt: expr); } } else { var result = VisitRvalueWithState(expr); if (_returnTypesOpt != null) { _returnTypesOpt.Add((node, result.ToTypeWithAnnotations(compilation))); } } EnforceDoesNotReturn(node.Syntax); if (IsConditionalState) { var joinedState = this.StateWhenTrue.Clone(); Join(ref joinedState, ref this.StateWhenFalse); PendingBranches.Add(new PendingBranch(node, joinedState, label: null, this.IsConditionalState, this.StateWhenTrue, this.StateWhenFalse)); } else { PendingBranches.Add(new PendingBranch(node, this.State, label: null)); } Unsplit(); if (CurrentSymbol is MethodSymbol method) { EnforceNotNullIfNotNull(node.Syntax, this.State, method.Parameters, method.ReturnNotNullIfParameterNotNull, ResultType.State, outputParam: null); } SetUnreachable(); return null; } private TypeWithState VisitRefExpression(BoundExpression expr, TypeWithAnnotations destinationType) { Visit(expr); TypeWithState resultType = ResultType; if (!expr.IsSuppressed && RemoveConversion(expr, includeExplicitConversions: false).expression.Kind != BoundKind.ThrowExpression) { var lvalueResultType = LvalueResultType; if (IsNullabilityMismatch(lvalueResultType, destinationType)) { // declared types must match ReportNullabilityMismatchInAssignment(expr.Syntax, lvalueResultType, destinationType); } else { // types match, but state would let a null in ReportNullableAssignmentIfNecessary(expr, destinationType, resultType, useLegacyWarnings: false); } } return resultType; } private bool TryGetReturnType(out TypeWithAnnotations type, out FlowAnalysisAnnotations annotations) { var method = CurrentSymbol as MethodSymbol; if (method is null) { type = default; annotations = FlowAnalysisAnnotations.None; return false; } var delegateOrMethod = _delegateInvokeMethod ?? method; var returnType = delegateOrMethod.ReturnTypeWithAnnotations; Debug.Assert((object)returnType != LambdaSymbol.ReturnTypeIsBeingInferred); if (returnType.IsVoidType()) { type = default; annotations = FlowAnalysisAnnotations.None; return false; } if (!method.IsAsync) { annotations = delegateOrMethod.ReturnTypeFlowAnalysisAnnotations; type = ApplyUnconditionalAnnotations(returnType, annotations); return true; } if (returnType.Type.IsGenericTaskType(compilation)) { type = ((NamedTypeSymbol)returnType.Type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics.Single(); annotations = FlowAnalysisAnnotations.None; return true; } type = default; annotations = FlowAnalysisAnnotations.None; return false; } public override BoundNode? VisitLocal(BoundLocal node) { var local = node.LocalSymbol; int slot = GetOrCreateSlot(local); var type = GetDeclaredLocalResult(local); if (!node.Type.Equals(type.Type, TypeCompareKind.ConsiderEverything | TypeCompareKind.IgnoreNullableModifiersForReferenceTypes | TypeCompareKind.IgnoreDynamicAndTupleNames)) { // When the local is used before or during initialization, there can potentially be a mismatch between node.LocalSymbol.Type and node.Type. We // need to prefer node.Type as we shouldn't be changing the type of the BoundLocal node during rewrite. // https://github.com/dotnet/roslyn/issues/34158 Debug.Assert(node.Type.IsErrorType() || type.Type.IsErrorType()); type = TypeWithAnnotations.Create(node.Type, type.NullableAnnotation); } SetResult(node, GetAdjustedResult(type.ToTypeWithState(), slot), type); return null; } public override BoundNode? VisitBlock(BoundBlock node) { DeclareLocals(node.Locals); VisitStatementsWithLocalFunctions(node); return null; } private void VisitStatementsWithLocalFunctions(BoundBlock block) { // Since the nullable flow state affects type information, and types can be queried by // the semantic model, there needs to be a single flow state input to a local function // that cannot be path-dependent. To decide the local starting state we Meet the state // of captured variables from all the uses of the local function, computing the // conservative combination of all potential starting states. // // For performance we split the analysis into two phases: the first phase where we // analyze everything except the local functions, hoping to visit all of the uses of the // local function, and then a pass where we visit the local functions. If there's no // recursion or calls between the local functions, the starting state of the local // function should be stable and we don't need a second pass. if (!TrackingRegions && !block.LocalFunctions.IsDefaultOrEmpty) { // First visit everything else foreach (var stmt in block.Statements) { if (stmt.Kind != BoundKind.LocalFunctionStatement) { VisitStatement(stmt); } } // Now visit the local function bodies foreach (var stmt in block.Statements) { if (stmt is BoundLocalFunctionStatement localFunc) { VisitLocalFunctionStatement(localFunc); } } } else { foreach (var stmt in block.Statements) { VisitStatement(stmt); } } } public override BoundNode? VisitLocalFunctionStatement(BoundLocalFunctionStatement node) { var localFunc = node.Symbol; var localFunctionState = GetOrCreateLocalFuncUsages(localFunc); // The starting state is the top state, but with captured // variables set according to Joining the state at all the // local function use sites var state = TopState(); for (int slot = 1; slot < localFunctionState.StartingState.Capacity; slot++) { var symbol = variableBySlot[RootSlot(slot)].Symbol; if (Symbol.IsCaptured(symbol, localFunc)) { state[slot] = localFunctionState.StartingState[slot]; } } localFunctionState.Visited = true; AnalyzeLocalFunctionOrLambda( node, localFunc, state, delegateInvokeMethod: null, useDelegateInvokeParameterTypes: false); SetInvalidResult(); return null; } private void AnalyzeLocalFunctionOrLambda( IBoundLambdaOrFunction lambdaOrFunction, MethodSymbol lambdaOrFunctionSymbol, LocalState state, MethodSymbol? delegateInvokeMethod, bool useDelegateInvokeParameterTypes) { var oldSymbol = this.CurrentSymbol; this.CurrentSymbol = lambdaOrFunctionSymbol; Debug.Assert(!useDelegateInvokeParameterTypes || delegateInvokeMethod is object); var oldDelegateInvokeMethod = _delegateInvokeMethod; _delegateInvokeMethod = delegateInvokeMethod; var oldUseDelegateInvokeParameterTypes = _useDelegateInvokeParameterTypes; _useDelegateInvokeParameterTypes = useDelegateInvokeParameterTypes; var oldReturnTypes = _returnTypesOpt; _returnTypesOpt = null; var oldState = this.State; this.State = state; var previousSlot = _snapshotBuilderOpt?.EnterNewWalker(lambdaOrFunctionSymbol) ?? -1; var oldPending = SavePending(); EnterParameters(); var oldPending2 = SavePending(); // If this is an iterator, there's an implicit branch before the first statement // of the function where the enumerable is returned. if (lambdaOrFunctionSymbol.IsIterator) { PendingBranches.Add(new PendingBranch(null, this.State, null)); } VisitAlways(lambdaOrFunction.Body); RestorePending(oldPending2); // process any forward branches within the lambda body ImmutableArray pendingReturns = RemoveReturns(); RestorePending(oldPending); var location = lambdaOrFunctionSymbol.Locations.FirstOrNone(); LeaveParameters(lambdaOrFunctionSymbol.Parameters, lambdaOrFunction.Syntax, location); // Intersect the state of all branches out of the local function var stateAtReturn = this.State; foreach (PendingBranch pending in pendingReturns) { this.State = pending.State; BoundNode branch = pending.Branch; // Pass the local function identifier as a location if the branch // is null or compiler generated. LeaveParameters(lambdaOrFunctionSymbol.Parameters, branch?.Syntax, branch?.WasCompilerGenerated == false ? null : location); Join(ref stateAtReturn, ref this.State); } _snapshotBuilderOpt?.ExitWalker(this.SaveSharedState(), previousSlot); this.State = oldState; _returnTypesOpt = oldReturnTypes; _useDelegateInvokeParameterTypes = oldUseDelegateInvokeParameterTypes; _delegateInvokeMethod = oldDelegateInvokeMethod; this.CurrentSymbol = oldSymbol; } protected override void VisitLocalFunctionUse( LocalFunctionSymbol symbol, LocalFunctionState localFunctionState, SyntaxNode syntax, bool isCall) { // Do not use this overload in NullableWalker. Use the overload below instead. throw ExceptionUtilities.Unreachable; } private void VisitLocalFunctionUse(LocalFunctionSymbol symbol) { Debug.Assert(!IsConditionalState); var localFunctionState = GetOrCreateLocalFuncUsages(symbol); if (Join(ref localFunctionState.StartingState, ref State) && localFunctionState.Visited) { // If the starting state of the local function has changed and we've already visited // the local function, we need another pass stateChangedAfterUse = true; } } public override BoundNode? VisitDoStatement(BoundDoStatement node) { DeclareLocals(node.Locals); return base.VisitDoStatement(node); } public override BoundNode? VisitWhileStatement(BoundWhileStatement node) { DeclareLocals(node.Locals); return base.VisitWhileStatement(node); } public override BoundNode? VisitWithExpression(BoundWithExpression withExpr) { Debug.Assert(!IsConditionalState); var receiver = withExpr.Receiver; VisitRvalue(receiver); _ = CheckPossibleNullReceiver(receiver); var resultType = withExpr.CloneMethod?.ReturnTypeWithAnnotations ?? ResultType.ToTypeWithAnnotations(compilation); var resultState = ApplyUnconditionalAnnotations(resultType.ToTypeWithState(), GetRValueAnnotations(withExpr.CloneMethod)); var resultSlot = GetOrCreatePlaceholderSlot(withExpr); // carry over the null state of members of 'receiver' to the result of the with-expression. TrackNullableStateForAssignment(receiver, resultType, resultSlot, resultState, MakeSlot(receiver)); // use the declared nullability of Clone() for the top-level nullability of the result of the with-expression. SetResult(withExpr, resultState, resultType); VisitObjectCreationInitializer(containingSymbol: null, resultSlot, withExpr.InitializerExpression, FlowAnalysisAnnotations.None); // Note: this does not account for the scenario where `Clone()` returns maybe-null and the with-expression has no initializers. // Tracking in https://github.com/dotnet/roslyn/issues/44759 return null; } public override BoundNode? VisitForStatement(BoundForStatement node) { DeclareLocals(node.OuterLocals); DeclareLocals(node.InnerLocals); return base.VisitForStatement(node); } public override BoundNode? VisitForEachStatement(BoundForEachStatement node) { DeclareLocals(node.IterationVariables); Visit(node.AwaitOpt); return base.VisitForEachStatement(node); } public override BoundNode? VisitUsingStatement(BoundUsingStatement node) { DeclareLocals(node.Locals); Visit(node.AwaitOpt); return base.VisitUsingStatement(node); } public override BoundNode? VisitUsingLocalDeclarations(BoundUsingLocalDeclarations node) { Visit(node.AwaitOpt); return base.VisitUsingLocalDeclarations(node); } public override BoundNode? VisitFixedStatement(BoundFixedStatement node) { DeclareLocals(node.Locals); return base.VisitFixedStatement(node); } public override BoundNode? VisitConstructorMethodBody(BoundConstructorMethodBody node) { DeclareLocals(node.Locals); return base.VisitConstructorMethodBody(node); } private void DeclareLocal(LocalSymbol local) { if (local.DeclarationKind != LocalDeclarationKind.None) { int slot = GetOrCreateSlot(local); if (slot > 0) { PopulateOneSlot(ref this.State, slot); InheritDefaultState(GetDeclaredLocalResult(local).Type, slot); } } } private void DeclareLocals(ImmutableArray locals) { foreach (var local in locals) { DeclareLocal(local); } } public override BoundNode? VisitLocalDeclaration(BoundLocalDeclaration node) { var local = node.LocalSymbol; int slot = GetOrCreateSlot(local); // We need visit the optional arguments so that we can return nullability information // about them, but we don't want to communicate any information about anything underneath. // Additionally, tests like Scope_DeclaratorArguments_06 can have conditional expressions // in the optional arguments that can leave us in a split state, so we want to make sure // we are not in a conditional state after. Debug.Assert(!IsConditionalState); var oldDisable = _disableDiagnostics; _disableDiagnostics = true; var currentState = State; VisitAndUnsplitAll(node.ArgumentsOpt); _disableDiagnostics = oldDisable; SetState(currentState); if (node.DeclaredTypeOpt != null) { VisitTypeExpression(node.DeclaredTypeOpt); } var initializer = node.InitializerOpt; if (initializer is null) { return null; } TypeWithAnnotations type = local.TypeWithAnnotations; TypeWithState valueType; bool inferredType = node.InferredType; if (local.IsRef) { valueType = VisitRefExpression(initializer, type); } else { valueType = VisitOptionalImplicitConversion(initializer, targetTypeOpt: inferredType ? default : type, useLegacyWarnings: true, trackMembers: true, AssignmentKind.Assignment); } if (inferredType) { if (valueType.HasNullType) { Debug.Assert(type.Type.IsErrorType()); valueType = type.ToTypeWithState(); } type = valueType.ToAnnotatedTypeWithAnnotations(compilation); _variableTypes[local] = type; if (node.DeclaredTypeOpt != null) { SetAnalyzedNullability(node.DeclaredTypeOpt, new VisitResult(type.ToTypeWithState(), type), true); } } TrackNullableStateForAssignment(initializer, type, slot, valueType, MakeSlot(initializer)); return null; } protected override BoundExpression? VisitExpressionWithoutStackGuard(BoundExpression node) { Debug.Assert(!IsConditionalState); SetInvalidResult(); _ = base.VisitExpressionWithoutStackGuard(node); TypeWithState resultType = ResultType; #if DEBUG // Verify Visit method set _result. Debug.Assert((object?)resultType.Type != _invalidType.Type); Debug.Assert(AreCloseEnough(resultType.Type, node.Type)); #endif if (ShouldMakeNotNullRvalue(node)) { var result = resultType.WithNotNullState(); SetResult(node, result, LvalueResultType); } return null; } #if DEBUG // For asserts only. private static bool AreCloseEnough(TypeSymbol? typeA, TypeSymbol? typeB) { // https://github.com/dotnet/roslyn/issues/34993: We should be able to tighten this to ensure that we're actually always returning the same type, // not error if one is null or ignoring certain types if ((object?)typeA == typeB) { return true; } if (typeA is null || typeB is null) { return typeA?.IsErrorType() != false && typeB?.IsErrorType() != false; } return canIgnoreAnyType(typeA) || canIgnoreAnyType(typeB) || typeA.Equals(typeB, TypeCompareKind.IgnoreCustomModifiersAndArraySizesAndLowerBounds | TypeCompareKind.IgnoreNullableModifiersForReferenceTypes | TypeCompareKind.IgnoreDynamicAndTupleNames); // Ignore TupleElementNames (see https://github.com/dotnet/roslyn/issues/23651). bool canIgnoreAnyType(TypeSymbol type) { return type.VisitType((t, unused1, unused2) => canIgnoreType(t), (object?)null) is object; } bool canIgnoreType(TypeSymbol type) { return type.IsErrorType() || type.IsDynamic() || type.HasUseSiteError || (type.IsAnonymousType && canIgnoreAnonymousType((NamedTypeSymbol)type)); } bool canIgnoreAnonymousType(NamedTypeSymbol type) { return AnonymousTypeManager.GetAnonymousTypePropertyTypesWithAnnotations(type).Any(t => canIgnoreAnyType(t.Type)); } } private static bool AreCloseEnough(Symbol original, Symbol updated) { // When https://github.com/dotnet/roslyn/issues/38195 is fixed, this workaround needs to be removed if (original is ConstructedMethodSymbol || updated is ConstructedMethodSymbol) { return AreCloseEnough(original.OriginalDefinition, updated.OriginalDefinition); } return (original, updated) switch { (LambdaSymbol l, NamedTypeSymbol n) _ when n.IsDelegateType() => AreLambdaAndNewDelegateSimilar(l, n), (FieldSymbol { ContainingType: { IsTupleType: true }, TupleElementIndex: var oi } originalField, FieldSymbol { ContainingType: { IsTupleType: true }, TupleElementIndex: var ui } updatedField) => originalField.Type.Equals(updatedField.Type, TypeCompareKind.AllNullableIgnoreOptions | TypeCompareKind.IgnoreTupleNames) && oi == ui, _ => original.Equals(updated, TypeCompareKind.AllNullableIgnoreOptions | TypeCompareKind.IgnoreTupleNames) }; } #endif private static bool AreLambdaAndNewDelegateSimilar(LambdaSymbol l, NamedTypeSymbol n) { var invokeMethod = n.DelegateInvokeMethod; return invokeMethod.Parameters.SequenceEqual(l.Parameters, (p1, p2) => p1.Type.Equals(p2.Type, TypeCompareKind.AllNullableIgnoreOptions | TypeCompareKind.IgnoreTupleNames)) && invokeMethod.ReturnType.Equals(l.ReturnType, TypeCompareKind.AllNullableIgnoreOptions | TypeCompareKind.IgnoreTupleNames); } public override BoundNode? Visit(BoundNode? node) { return Visit(node, expressionIsRead: true); } private BoundNode VisitLValue(BoundNode node) { return Visit(node, expressionIsRead: false); } private BoundNode Visit(BoundNode? node, bool expressionIsRead) { bool originalExpressionIsRead = _expressionIsRead; _expressionIsRead = expressionIsRead; TakeIncrementalSnapshot(node); var result = base.Visit(node); _expressionIsRead = originalExpressionIsRead; return result; } protected override void VisitStatement(BoundStatement statement) { SetInvalidResult(); base.VisitStatement(statement); SetInvalidResult(); } public override BoundNode? VisitObjectCreationExpression(BoundObjectCreationExpression node) { Debug.Assert(!IsConditionalState); var arguments = node.Arguments; var argumentResults = VisitArguments(node, arguments, node.ArgumentRefKindsOpt, node.Constructor, node.ArgsToParamsOpt, node.Expanded, invokedAsExtensionMethod: false).results; VisitObjectOrDynamicObjectCreation(node, arguments, argumentResults, node.InitializerExpressionOpt); return null; } public override BoundNode? VisitUnconvertedObjectCreationExpression(BoundUnconvertedObjectCreationExpression node) { // This method is only involved in method inference with unbound lambdas. // The diagnostics on arguments are reported by VisitObjectCreationExpression. SetResultType(node, TypeWithState.Create(null, NullableFlowState.NotNull)); return null; } private void VisitObjectOrDynamicObjectCreation( BoundExpression node, ImmutableArray arguments, ImmutableArray argumentResults, BoundExpression? initializerOpt) { Debug.Assert(node.Kind == BoundKind.ObjectCreationExpression || node.Kind == BoundKind.DynamicObjectCreationExpression || node.Kind == BoundKind.NewT); var argumentTypes = argumentResults.SelectAsArray(ar => ar.RValueType); int slot = -1; var type = node.Type; var resultState = NullableFlowState.NotNull; if (type is object) { slot = GetOrCreatePlaceholderSlot(node); if (slot > 0) { var boundObjectCreationExpression = node as BoundObjectCreationExpression; var constructor = boundObjectCreationExpression?.Constructor; bool isDefaultValueTypeConstructor = constructor?.IsDefaultValueTypeConstructor() == true; if (EmptyStructTypeCache.IsTrackableStructType(type)) { var containingType = constructor?.ContainingType; if (containingType?.IsTupleType == true && !isDefaultValueTypeConstructor) { // new System.ValueTuple(e1, ..., eN) TrackNullableStateOfTupleElements(slot, containingType, arguments, argumentTypes, boundObjectCreationExpression!.ArgsToParamsOpt, useRestField: true); } else { InheritNullableStateOfTrackableStruct( type, slot, valueSlot: -1, isDefaultValue: isDefaultValueTypeConstructor); } } else if (type.IsNullableType()) { if (isDefaultValueTypeConstructor) { // a nullable value type created with its default constructor is by definition null resultState = NullableFlowState.MaybeNull; } else if (constructor?.ParameterCount == 1) { // if we deal with one-parameter ctor that takes underlying, then Value state is inferred from the argument. var parameterType = constructor.ParameterTypesWithAnnotations[0]; if (AreNullableAndUnderlyingTypes(type, parameterType.Type, out TypeWithAnnotations underlyingType)) { var operand = arguments[0]; int valueSlot = MakeSlot(operand); if (valueSlot > 0) { TrackNullableStateOfNullableValue(slot, type, operand, underlyingType.ToTypeWithState(), valueSlot); } } } } this.State[slot] = resultState; } } if (initializerOpt != null) { VisitObjectCreationInitializer(containingSymbol: null, slot, initializerOpt, leftAnnotations: FlowAnalysisAnnotations.None); } SetResultType(node, TypeWithState.Create(type, resultState)); } private void VisitObjectCreationInitializer(Symbol? containingSymbol, int containingSlot, BoundExpression node, FlowAnalysisAnnotations leftAnnotations) { TakeIncrementalSnapshot(node); switch (node) { case BoundObjectInitializerExpression objectInitializer: checkImplicitReceiver(objectInitializer); foreach (var initializer in objectInitializer.Initializers) { switch (initializer.Kind) { case BoundKind.AssignmentOperator: VisitObjectElementInitializer(containingSlot, (BoundAssignmentOperator)initializer); break; default: VisitRvalue(initializer); break; } } SetNotNullResult(objectInitializer.Placeholder); break; case BoundCollectionInitializerExpression collectionInitializer: checkImplicitReceiver(collectionInitializer); foreach (var initializer in collectionInitializer.Initializers) { switch (initializer.Kind) { case BoundKind.CollectionElementInitializer: VisitCollectionElementInitializer((BoundCollectionElementInitializer)initializer); break; default: VisitRvalue(initializer); break; } } SetNotNullResult(collectionInitializer.Placeholder); break; default: Debug.Assert(containingSymbol is object); if (containingSymbol is object) { var type = ApplyLValueAnnotations(containingSymbol.GetTypeOrReturnType(), leftAnnotations); TypeWithState resultType = VisitOptionalImplicitConversion(node, type, useLegacyWarnings: false, trackMembers: true, AssignmentKind.Assignment); TrackNullableStateForAssignment(node, type, containingSlot, resultType, MakeSlot(node)); } break; } void checkImplicitReceiver(BoundObjectInitializerExpressionBase node) { if (containingSlot >= 0 && !node.Initializers.IsEmpty) { if (!node.Type.IsValueType && State[containingSlot].MayBeNull()) { if (containingSymbol is null) { ReportDiagnostic(ErrorCode.WRN_NullReferenceReceiver, node.Syntax); } else { ReportDiagnostic(ErrorCode.WRN_NullReferenceInitializer, node.Syntax, containingSymbol); } } } } } private void VisitObjectElementInitializer(int containingSlot, BoundAssignmentOperator node) { TakeIncrementalSnapshot(node); var left = node.Left; switch (left.Kind) { case BoundKind.ObjectInitializerMember: { var objectInitializer = (BoundObjectInitializerMember)left; TakeIncrementalSnapshot(left); var symbol = objectInitializer.MemberSymbol; if (!objectInitializer.Arguments.IsDefaultOrEmpty) { VisitArguments(objectInitializer, objectInitializer.Arguments, objectInitializer.ArgumentRefKindsOpt, (PropertySymbol?)symbol, objectInitializer.ArgsToParamsOpt, objectInitializer.Expanded); } if (symbol is object) { int slot = (containingSlot < 0 || !IsSlotMember(containingSlot, symbol)) ? -1 : GetOrCreateSlot(symbol, containingSlot); VisitObjectCreationInitializer(symbol, slot, node.Right, GetLValueAnnotations(node.Left)); // https://github.com/dotnet/roslyn/issues/35040: Should likely be setting _resultType in VisitObjectCreationInitializer // and using that value instead of reconstructing here } var result = new VisitResult(objectInitializer.Type, NullableAnnotation.NotAnnotated, NullableFlowState.NotNull); SetAnalyzedNullability(objectInitializer, result); SetAnalyzedNullability(node, result); } break; default: Visit(node); break; } } private new void VisitCollectionElementInitializer(BoundCollectionElementInitializer node) { // Note: we analyze even omitted calls (var reinferredMethod, _, _) = VisitArguments(node, node.Arguments, refKindsOpt: default, node.AddMethod, node.ArgsToParamsOpt, node.Expanded, node.InvokedAsExtensionMethod); Debug.Assert(reinferredMethod is object); if (node.ImplicitReceiverOpt != null) { Debug.Assert(node.ImplicitReceiverOpt.Kind == BoundKind.ObjectOrCollectionValuePlaceholder); SetAnalyzedNullability(node.ImplicitReceiverOpt, new VisitResult(node.ImplicitReceiverOpt.Type, NullableAnnotation.NotAnnotated, NullableFlowState.NotNull)); } SetUnknownResultNullability(node); SetUpdatedSymbol(node, node.AddMethod, reinferredMethod); } private void SetNotNullResult(BoundExpression node) { SetResultType(node, TypeWithState.Create(node.Type, NullableFlowState.NotNull)); } /// /// Returns true if the type is a struct with no fields or properties. /// protected override bool IsEmptyStructType(TypeSymbol type) { if (type.TypeKind != TypeKind.Struct) { return false; } // EmptyStructTypeCache.IsEmptyStructType() returns false // if there are non-cyclic fields. if (!_emptyStructTypeCache.IsEmptyStructType(type)) { return false; } if (type.SpecialType != SpecialType.None) { return true; } var members = ((NamedTypeSymbol)type).GetMembersUnordered(); // EmptyStructTypeCache.IsEmptyStructType() returned true. If there are fields, // at least one of those fields must be cyclic, so treat the type as empty. if (members.Any(m => m.Kind == SymbolKind.Field)) { return true; } // If there are properties, the type is not empty. if (members.Any(m => m.Kind == SymbolKind.Property)) { return false; } return true; } private int GetOrCreatePlaceholderSlot(BoundExpression node) { Debug.Assert(node.Type is object); if (IsEmptyStructType(node.Type)) { return -1; } return GetOrCreatePlaceholderSlot(node, TypeWithAnnotations.Create(node.Type, NullableAnnotation.NotAnnotated)); } private int GetOrCreatePlaceholderSlot(object identifier, TypeWithAnnotations type) { _placeholderLocalsOpt ??= PooledDictionary.GetInstance(); if (!_placeholderLocalsOpt.TryGetValue(identifier, out var placeholder)) { placeholder = new PlaceholderLocal(CurrentSymbol, identifier, type); _placeholderLocalsOpt.Add(identifier, placeholder); } Debug.Assert((object)placeholder != null); return GetOrCreateSlot(placeholder, forceSlotEvenIfEmpty: true); } public override BoundNode? VisitAnonymousObjectCreationExpression(BoundAnonymousObjectCreationExpression node) { Debug.Assert(!IsConditionalState); Debug.Assert(node.Type.IsAnonymousType); var anonymousType = (NamedTypeSymbol)node.Type; var arguments = node.Arguments; var argumentTypes = arguments.SelectAsArray((arg, self) => self.VisitRvalueWithState(arg), this); var argumentsWithAnnotations = argumentTypes.SelectAsArray(arg => arg.ToTypeWithAnnotations(compilation)); if (argumentsWithAnnotations.All(argType => argType.HasType)) { anonymousType = AnonymousTypeManager.ConstructAnonymousTypeSymbol(anonymousType, argumentsWithAnnotations); int receiverSlot = GetOrCreatePlaceholderSlot(node); int currentDeclarationIndex = 0; for (int i = 0; i < arguments.Length; i++) { var argument = arguments[i]; var argumentType = argumentTypes[i]; var property = AnonymousTypeManager.GetAnonymousTypeProperty(anonymousType, i); if (property.Type.SpecialType != SpecialType.System_Void) { // A void element results in an error type in the anonymous type but not in the property's container! // To avoid failing an assertion later, we skip them. var slot = GetOrCreateSlot(property, receiverSlot); TrackNullableStateForAssignment(argument, property.TypeWithAnnotations, slot, argumentType, MakeSlot(argument)); var currentDeclaration = getDeclaration(node, property, ref currentDeclarationIndex); if (currentDeclaration is object) { TakeIncrementalSnapshot(currentDeclaration); SetAnalyzedNullability(currentDeclaration, new VisitResult(argumentType, property.TypeWithAnnotations)); } } } } SetResultType(node, TypeWithState.Create(anonymousType, NullableFlowState.NotNull)); return null; static BoundAnonymousPropertyDeclaration? getDeclaration(BoundAnonymousObjectCreationExpression node, PropertySymbol currentProperty, ref int currentDeclarationIndex) { if (currentDeclarationIndex >= node.Declarations.Length) { return null; } var currentDeclaration = node.Declarations[currentDeclarationIndex]; if (currentDeclaration.Property.MemberIndexOpt == currentProperty.MemberIndexOpt) { currentDeclarationIndex++; return currentDeclaration; } return null; } } public override BoundNode? VisitArrayCreation(BoundArrayCreation node) { foreach (var expr in node.Bounds) { VisitRvalue(expr); } var initialization = node.InitializerOpt; if (initialization is null) { SetResultType(node, TypeWithState.Create(node.Type, NullableFlowState.NotNull)); return null; } TakeIncrementalSnapshot(initialization); 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.ElementTypeWithAnnotations; if (!isInferred) { foreach (var expr in expressions) { _ = VisitOptionalImplicitConversion(expr, elementType, useLegacyWarnings: false, trackMembers: false, AssignmentKind.Assignment); } } else { var expressionsNoConversions = ArrayBuilder.GetInstance(n); var conversions = ArrayBuilder.GetInstance(n); var resultTypes = ArrayBuilder.GetInstance(n); var placeholderBuilder = ArrayBuilder.GetInstance(n); foreach (var expression in expressions) { // collect expressions, conversions and result types (BoundExpression expressionNoConversion, Conversion conversion) = RemoveConversion(expression, includeExplicitConversions: false); expressionsNoConversions.Add(expressionNoConversion); conversions.Add(conversion); SnapshotWalkerThroughConversionGroup(expression, expressionNoConversion); var resultType = VisitRvalueWithState(expressionNoConversion); resultTypes.Add(resultType); placeholderBuilder.Add(CreatePlaceholderIfNecessary(expressionNoConversion, resultType.ToTypeWithAnnotations(compilation))); } var placeholders = placeholderBuilder.ToImmutableAndFree(); TypeSymbol? bestType = null; if (!node.HasErrors) { HashSet? useSiteDiagnostics = null; bestType = BestTypeInferrer.InferBestType(placeholders, _conversions, ref useSiteDiagnostics); } TypeWithAnnotations inferredType = (bestType is null) ? elementType.SetUnknownNullabilityForReferenceTypes() : TypeWithAnnotations.Create(bestType); if (bestType is object) { // Convert elements to best type to determine element top-level nullability and to report nested nullability warnings for (int i = 0; i < n; i++) { var expressionNoConversion = expressionsNoConversions[i]; var expression = GetConversionIfApplicable(expressions[i], expressionNoConversion); resultTypes[i] = VisitConversion(expression, expressionNoConversion, conversions[i], inferredType, resultTypes[i], checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: true, reportTopLevelWarnings: false); } // Set top-level nullability on inferred element type var elementState = BestTypeInferrer.GetNullableState(resultTypes); inferredType = TypeWithState.Create(inferredType.Type, elementState).ToTypeWithAnnotations(compilation); for (int i = 0; i < n; i++) { // Report top-level warnings _ = VisitConversion(conversionOpt: null, conversionOperand: expressionsNoConversions[i], Conversion.Identity, targetTypeWithNullability: inferredType, operandType: resultTypes[i], checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportRemainingWarnings: false); } } else { // We need to ensure that we're tracking the inferred type with nullability of any conversions that // were stripped off. for (int i = 0; i < n; i++) { TrackAnalyzedNullabilityThroughConversionGroup(inferredType.ToTypeWithState(), expressions[i] as BoundConversion, expressionsNoConversions[i]); } } expressionsNoConversions.Free(); conversions.Free(); resultTypes.Free(); arrayType = arrayType.WithElementType(inferredType); } expressions.Free(); SetResultType(node, TypeWithState.Create(arrayType, NullableFlowState.NotNull)); return null; } /// /// Applies analysis 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 TypeWithAnnotations BestTypeForLambdaReturns( ArrayBuilder<(BoundExpression, TypeWithAnnotations)> returns, CSharpCompilation compilation, BoundNode node, Conversions conversions) { var walker = new NullableWalker(compilation, symbol: null, useConstructorExitWarnings: false, useDelegateInvokeParameterTypes: false, delegateInvokeMethodOpt: null, node, binder: null, // Never used conversions: conversions, initialState: null, returnTypesOpt: null, analyzedNullabilityMapOpt: null, snapshotBuilderOpt: null); int n = returns.Count; var resultTypes = ArrayBuilder.GetInstance(n); var placeholdersBuilder = ArrayBuilder.GetInstance(n); for (int i = 0; i < n; i++) { var (returnExpr, resultType) = returns[i]; resultTypes.Add(resultType); placeholdersBuilder.Add(CreatePlaceholderIfNecessary(returnExpr, resultType)); } HashSet? useSiteDiagnostics = null; var placeholders = placeholdersBuilder.ToImmutableAndFree(); TypeSymbol bestType = BestTypeInferrer.InferBestType(placeholders, walker._conversions, ref useSiteDiagnostics); TypeWithAnnotations inferredType; if ((object)bestType != null) { // Note: so long as we have a best type, we can proceed. var bestTypeWithObliviousAnnotation = TypeWithAnnotations.Create(bestType); ConversionsBase conversionsWithoutNullability = walker._conversions.WithNullability(false); for (int i = 0; i < n; i++) { BoundExpression placeholder = placeholders[i]; Conversion conversion = conversionsWithoutNullability.ClassifyConversionFromExpression(placeholder, bestType, ref useSiteDiagnostics); resultTypes[i] = walker.VisitConversion(conversionOpt: null, placeholder, conversion, bestTypeWithObliviousAnnotation, resultTypes[i].ToTypeWithState(), checkConversion: false, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Return, reportRemainingWarnings: false, reportTopLevelWarnings: false).ToTypeWithAnnotations(compilation); } // Set top-level nullability on inferred type inferredType = TypeWithAnnotations.Create(bestType, BestTypeInferrer.GetNullableAnnotation(resultTypes)); } else { inferredType = default; } resultTypes.Free(); walker.Free(); return inferredType; } private static void GetArrayElements(BoundArrayInitialization node, ArrayBuilder builder) { foreach (var child in node.Initializers) { if (child.Kind == BoundKind.ArrayInitialization) { GetArrayElements((BoundArrayInitialization)child, builder); } else { builder.Add(child); } } } public override BoundNode? VisitArrayAccess(BoundArrayAccess node) { Debug.Assert(!IsConditionalState); Visit(node.Expression); Debug.Assert(!IsConditionalState); Debug.Assert(!node.Expression.Type!.IsValueType); // https://github.com/dotnet/roslyn/issues/30598: Mark receiver as not null // after indices have been visited, and only if the receiver has not changed. _ = CheckPossibleNullReceiver(node.Expression); var type = ResultType.Type as ArrayTypeSymbol; foreach (var i in node.Indices) { VisitRvalue(i); } TypeWithAnnotations result; if (node.Indices.Length == 1 && TypeSymbol.Equals(node.Indices[0].Type, compilation.GetWellKnownType(WellKnownType.System_Range), TypeCompareKind.ConsiderEverything2)) { result = TypeWithAnnotations.Create(type); } else { result = type?.ElementTypeWithAnnotations ?? default; } SetLvalueResultType(node, result); return null; } private TypeWithState InferResultNullability(BinaryOperatorKind operatorKind, MethodSymbol? methodOpt, TypeSymbol resultType, TypeWithState leftType, TypeWithState rightType) { NullableFlowState resultState = NullableFlowState.NotNull; if (operatorKind.IsUserDefined()) { if (methodOpt?.ParameterCount == 2) { if (operatorKind.IsLifted() && !operatorKind.IsComparison()) { return GetLiftedReturnType(methodOpt.ReturnTypeWithAnnotations, leftType.State.Join(rightType.State)); } var resultTypeWithState = GetReturnTypeWithState(methodOpt); if ((leftType.IsNotNull && methodOpt.ReturnNotNullIfParameterNotNull.Contains(methodOpt.Parameters[0].Name)) || (rightType.IsNotNull && methodOpt.ReturnNotNullIfParameterNotNull.Contains(methodOpt.Parameters[1].Name))) { resultTypeWithState = resultTypeWithState.WithNotNullState(); } return resultTypeWithState; } } else if (!operatorKind.IsDynamic() && !resultType.IsValueType) { switch (operatorKind.Operator() | operatorKind.OperandTypes()) { case BinaryOperatorKind.DelegateCombination: resultState = leftType.State.Meet(rightType.State); break; case BinaryOperatorKind.DelegateRemoval: resultState = NullableFlowState.MaybeNull; // Delegate removal can produce null. break; default: resultState = NullableFlowState.NotNull; break; } } if (operatorKind.IsLifted() && !operatorKind.IsComparison()) { resultState = leftType.State.Join(rightType.State); } return TypeWithState.Create(resultType, resultState); } protected override void VisitBinaryOperatorChildren(ArrayBuilder stack) { var binary = stack.Pop(); var (leftOperand, leftConversion) = RemoveConversion(binary.Left, includeExplicitConversions: false); Visit(leftOperand); while (true) { if (!learnFromBooleanConstantTest()) { Unsplit(); // VisitRvalue does this UseRvalueOnly(leftOperand); // drop lvalue part AfterLeftChildHasBeenVisited(leftOperand, leftConversion, binary); } if (stack.Count == 0) { break; } leftOperand = binary; leftConversion = Conversion.Identity; binary = stack.Pop(); } // learn from true/false constant bool learnFromBooleanConstantTest() { if (!IsConditionalState) { return false; } if (!leftConversion.IsIdentity) { return false; } BinaryOperatorKind op = binary.OperatorKind.Operator(); if (op != BinaryOperatorKind.Equal && op != BinaryOperatorKind.NotEqual) { return false; } bool isSense; if (binary.Right.ConstantValue?.IsBoolean == true) { UseRvalueOnly(leftOperand); // record result for the left var stateWhenTrue = this.StateWhenTrue.Clone(); var stateWhenFalse = this.StateWhenFalse.Clone(); Unsplit(); Visit(binary.Right); UseRvalueOnly(binary.Right); // record result for the right SetConditionalState(stateWhenTrue, stateWhenFalse); isSense = (op == BinaryOperatorKind.Equal) == binary.Right.ConstantValue.BooleanValue; } else if (binary.Left.ConstantValue?.IsBoolean == true) { Unsplit(); UseRvalueOnly(leftOperand); // record result for the left Visit(binary.Right); UseRvalueOnly(binary.Right); // record result for the right isSense = (op == BinaryOperatorKind.Equal) == binary.Left.ConstantValue.BooleanValue; } else { return false; } if (!isSense && IsConditionalState) { SetConditionalState(StateWhenFalse, StateWhenTrue); } // record result for the binary Debug.Assert(binary.Type.SpecialType == SpecialType.System_Boolean); SetResult(binary, TypeWithState.ForType(binary.Type), TypeWithAnnotations.Create(binary.Type)); return true; } } private void AfterLeftChildHasBeenVisited(BoundExpression leftOperand, Conversion leftConversion, BoundBinaryOperator binary) { Debug.Assert(!IsConditionalState); TypeWithState leftType = ResultType; var (rightOperand, rightConversion) = RemoveConversion(binary.Right, includeExplicitConversions: false); var rightType = VisitRvalueWithState(rightOperand); Debug.Assert(!IsConditionalState); // At this point, State.Reachable may be false for // invalid code such as `s + throw new Exception()`. var method = binary.MethodOpt; if (binary.OperatorKind.IsUserDefined() && method?.ParameterCount == 2) { // Update method based on inferred operand type. TypeSymbol methodContainer = method.ContainingType; bool isLifted = binary.OperatorKind.IsLifted(); TypeWithState leftUnderlyingType = GetNullableUnderlyingTypeIfNecessary(isLifted, leftType); TypeWithState rightUnderlyingType = GetNullableUnderlyingTypeIfNecessary(isLifted, rightType); TypeSymbol? asMemberOfType = getTypeIfContainingType(methodContainer, leftUnderlyingType.Type) ?? getTypeIfContainingType(methodContainer, rightUnderlyingType.Type); if (asMemberOfType is object) { method = (MethodSymbol)AsMemberOfType(asMemberOfType, method); } // Analyze operator call properly (honoring [Disallow|Allow|Maybe|NotNull] attribute annotations) https://github.com/dotnet/roslyn/issues/32671 var parameters = method.Parameters; visitOperandConversion(binary.Left, leftOperand, leftConversion, parameters[0], leftUnderlyingType); visitOperandConversion(binary.Right, rightOperand, rightConversion, parameters[1], rightUnderlyingType); SetUpdatedSymbol(binary, binary.MethodOpt!, method); void visitOperandConversion( BoundExpression expr, BoundExpression operand, Conversion conversion, ParameterSymbol parameter, TypeWithState operandType) { TypeWithAnnotations targetTypeWithNullability = parameter.TypeWithAnnotations; if (isLifted && targetTypeWithNullability.Type.IsNonNullableValueType()) { targetTypeWithNullability = TypeWithAnnotations.Create(MakeNullableOf(targetTypeWithNullability)); } _ = VisitConversion( expr as BoundConversion, operand, conversion, targetTypeWithNullability, operandType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Argument, parameter); } } else { // Assume this is a built-in operator in which case the parameter types are unannotated. visitOperandConversion(binary.Left, leftOperand, leftConversion, leftType); visitOperandConversion(binary.Right, rightOperand, rightConversion, rightType); void visitOperandConversion( BoundExpression expr, BoundExpression operand, Conversion conversion, TypeWithState operandType) { if (expr.Type is null) { Debug.Assert(operand == expr); } else { _ = VisitConversion( expr as BoundConversion, operand, conversion, TypeWithAnnotations.Create(expr.Type), operandType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Argument); } } } Debug.Assert(!IsConditionalState); // For nested binary operators, this can be the only time they're visited due to explicit stack used in AbstractFlowPass.VisitBinaryOperator, // so we need to set the flow-analyzed type here. var inferredResult = InferResultNullability(binary.OperatorKind, method, binary.Type, leftType, rightType); SetResult(binary, inferredResult, inferredResult.ToTypeWithAnnotations(compilation)); BinaryOperatorKind op = binary.OperatorKind.Operator(); if (op == BinaryOperatorKind.Equal || op == BinaryOperatorKind.NotEqual) { // learn from null constant BoundExpression? operandComparedToNull = null; if (binary.Right.ConstantValue?.IsNull == true) { operandComparedToNull = binary.Left; } else if (binary.Left.ConstantValue?.IsNull == true) { operandComparedToNull = binary.Right; } if (operandComparedToNull != null) { // Set all nested conditional slots. For example in a?.b?.c we'll set a, b, and c. bool nonNullCase = op != BinaryOperatorKind.Equal; // true represents WhenTrue splitAndLearnFromNonNullTest(operandComparedToNull, whenTrue: nonNullCase); // `x == null` and `x != null` are pure null tests so update the null-state in the alternative branch too LearnFromNullTest(operandComparedToNull, ref nonNullCase ? ref StateWhenFalse : ref StateWhenTrue); return; } } // learn from comparison between non-null and maybe-null, possibly updating maybe-null to non-null BoundExpression? operandComparedToNonNull = null; if (leftType.IsNotNull && rightType.MayBeNull) { operandComparedToNonNull = binary.Right; } else if (rightType.IsNotNull && leftType.MayBeNull) { operandComparedToNonNull = binary.Left; } if (operandComparedToNonNull != null) { switch (op) { case BinaryOperatorKind.Equal: case BinaryOperatorKind.GreaterThan: case BinaryOperatorKind.LessThan: case BinaryOperatorKind.GreaterThanOrEqual: case BinaryOperatorKind.LessThanOrEqual: operandComparedToNonNull = SkipReferenceConversions(operandComparedToNonNull); splitAndLearnFromNonNullTest(operandComparedToNonNull, whenTrue: true); return; case BinaryOperatorKind.NotEqual: operandComparedToNonNull = SkipReferenceConversions(operandComparedToNonNull); splitAndLearnFromNonNullTest(operandComparedToNonNull, whenTrue: false); return; }; } void splitAndLearnFromNonNullTest(BoundExpression operandComparedToNonNull, bool whenTrue) { var slotBuilder = ArrayBuilder.GetInstance(); GetSlotsToMarkAsNotNullable(operandComparedToNonNull, slotBuilder); if (slotBuilder.Count != 0) { Split(); ref LocalState stateToUpdate = ref whenTrue ? ref this.StateWhenTrue : ref this.StateWhenFalse; MarkSlotsAsNotNull(slotBuilder, ref stateToUpdate); } slotBuilder.Free(); } TypeSymbol? getTypeIfContainingType(TypeSymbol baseType, TypeSymbol? derivedType) { if (derivedType is null) { return null; } derivedType = derivedType.StrippedType(); HashSet? useSiteDiagnostics = null; var conversion = _conversions.ClassifyBuiltInConversion(derivedType, baseType, ref useSiteDiagnostics); if (conversion.Exists && !conversion.IsExplicit) { return derivedType; } return null; } } /// /// 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); var previousConditionalAccessSlot = _lastConditionalAccessSlot; try { while (true) { // Due to the nature of binding, if there are conditional access they will be at the top of the bound tree, // potentially with a conversion on top of it. We go through any conditional accesses, adding slots for the // conditional receivers if they have them. If we ever get to a receiver that MakeSlot doesn't return a slot // for, nothing underneath is trackable and we bail at that point. Example: // // a?.GetB()?.C // a is a field, GetB is a method, and C is a property // // The top of the tree is the a?.GetB() conditional call. We'll ask for a slot for a, and we'll get one because // fields have slots. The AccessExpression of the BoundConditionalAccess is another BoundConditionalAccess, this time // with a receiver of the GetB() BoundCall. Attempting to get a slot for this receiver will fail, and we'll // return an array with just the slot for a. int slot; switch (operand.Kind) { case BoundKind.Conversion: // https://github.com/dotnet/roslyn/issues/33879 Detect when conversion has a nullable operand operand = ((BoundConversion)operand).Operand; continue; case BoundKind.AsOperator: operand = ((BoundAsOperator)operand).Operand; continue; case BoundKind.ConditionalAccess: var conditional = (BoundConditionalAccess)operand; GetSlotsToMarkAsNotNullable(conditional.Receiver, slotBuilder); slot = MakeSlot(conditional.Receiver); if (slot > 0) { // We need to continue the walk regardless of whether the receiver should be updated. var receiverType = conditional.Receiver.Type!; if (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 // of MakeSlot, we substitute this slot when we encounter a BoundConditionalReceiver, and reset the // _lastConditionalAccess field. _lastConditionalAccessSlot = slot; operand = conditional.AccessExpression; continue; } // If there's no slot for this receiver, there cannot be another slot for any of the remaining // access expressions. break; default: // Attempt to create a slot for the current thing. If there were any more conditional accesses, // they would have been on top, so this is the last thing we need to specially handle. // https://github.com/dotnet/roslyn/issues/33879 When we handle unconditional access survival (ie after // c.D has been invoked, c must be nonnull or we've thrown a NullRef), revisit whether // we need more special handling here slot = MakeSlot(operand); if (slot > 0 && PossiblyNullableType(operand.Type)) { slotBuilder.Add(slot); } break; } return; } } finally { _lastConditionalAccessSlot = previousConditionalAccessSlot; } } private static bool PossiblyNullableType([NotNullWhen(true)] TypeSymbol? operandType) => operandType?.CanContainNull() == true; private static void MarkSlotsAsNotNull(ArrayBuilder slots, ref LocalState stateToUpdate) { foreach (int slot in slots) { stateToUpdate[slot] = NullableFlowState.NotNull; } } private void LearnFromNonNullTest(BoundExpression expression, ref LocalState state) { if (expression.Kind == BoundKind.AwaitableValuePlaceholder) { if (_awaitablePlaceholdersOpt != null && _awaitablePlaceholdersOpt.TryGetValue((BoundAwaitableValuePlaceholder)expression, out var value)) { expression = value.AwaitableExpression; } else { return; } } var slotBuilder = ArrayBuilder.GetInstance(); GetSlotsToMarkAsNotNullable(expression, slotBuilder); MarkSlotsAsNotNull(slotBuilder, ref state); slotBuilder.Free(); } private void LearnFromNonNullTest(int slot, ref LocalState state) { state[slot] = NullableFlowState.NotNull; } private void LearnFromNullTest(BoundExpression expression, ref LocalState state) { // nothing to learn about a constant if (expression.ConstantValue != null) return; // We should not blindly strip conversions here. Tracked by https://github.com/dotnet/roslyn/issues/36164 var expressionWithoutConversion = RemoveConversion(expression, includeExplicitConversions: true).expression; var slot = MakeSlot(expressionWithoutConversion); // Since we know for sure the slot is null (we just tested it), we know that dependent slots are not // reachable and therefore can be treated as not null. However, we have not computed the proper // (inferred) type for the expression, so we cannot compute the correct symbols for the member slots here // (using the incorrect symbols would result in computing an incorrect default state for them). // Therefore we do not mark dependent slots not null. See https://github.com/dotnet/roslyn/issues/39624 LearnFromNullTest(slot, expressionWithoutConversion.Type, ref state, markDependentSlotsNotNull: false); } private void LearnFromNullTest(int slot, TypeSymbol? expressionType, ref LocalState state, bool markDependentSlotsNotNull) { if (slot > 0 && PossiblyNullableType(expressionType)) { if (state[slot] == NullableFlowState.NotNull) { // Note: We leave a MaybeDefault state as-is state[slot] = NullableFlowState.MaybeNull; } if (markDependentSlotsNotNull) { MarkDependentSlotsNotNull(slot, expressionType, ref state); } } } // If we know for sure that a slot contains a null value, then we know for sure that dependent slots // are "unreachable" so we might as well treat them as not null. That way when this state is merged // with another state, those dependent states won't pollute values from the other state. private void MarkDependentSlotsNotNull(int slot, TypeSymbol expressionType, ref LocalState state, int depth = 2) { if (depth <= 0) return; foreach (var member in getMembers(expressionType)) { HashSet? discardedUseSiteDiagnostics = null; var containingType = this._symbol?.ContainingType; if ((member is PropertySymbol { IsIndexedProperty: false } || member.Kind == SymbolKind.Field) && member.RequiresInstanceReceiver() && (containingType is null || AccessCheck.IsSymbolAccessible(member, containingType, ref discardedUseSiteDiagnostics))) { int childSlot = GetOrCreateSlot(member, slot, true); if (childSlot > 0) { state[childSlot] = NullableFlowState.NotNull; MarkDependentSlotsNotNull(childSlot, member.GetTypeOrReturnType().Type, ref state, depth - 1); } } } static IEnumerable getMembers(TypeSymbol type) { // First, return the direct members foreach (var member in type.GetMembers()) yield return member; // All types inherit members from their effective bases for (NamedTypeSymbol baseType = effectiveBase(type); !(baseType is null); baseType = baseType.BaseTypeNoUseSiteDiagnostics) foreach (var member in baseType.GetMembers()) yield return member; // Interfaces and type parameters inherit from their effective interfaces foreach (NamedTypeSymbol interfaceType in inheritedInterfaces(type)) foreach (var member in interfaceType.GetMembers()) yield return member; yield break; static NamedTypeSymbol effectiveBase(TypeSymbol type) => type switch { TypeParameterSymbol tp => tp.EffectiveBaseClassNoUseSiteDiagnostics, var t => t.BaseTypeNoUseSiteDiagnostics, }; static ImmutableArray inheritedInterfaces(TypeSymbol type) => type switch { TypeParameterSymbol tp => tp.AllEffectiveInterfacesNoUseSiteDiagnostics, { TypeKind: TypeKind.Interface } => type.AllInterfacesNoUseSiteDiagnostics, _ => ImmutableArray.Empty, }; } } 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); TypeWithAnnotations targetType = VisitLvalueWithAnnotations(leftOperand); var leftState = this.State.Clone(); LearnFromNonNullTest(leftOperand, ref leftState); LearnFromNullTest(leftOperand, ref this.State); if (node.IsNullableValueTypeAssignment) { targetType = TypeWithAnnotations.Create(node.Type, NullableAnnotation.NotAnnotated); } TypeWithState rightResult = VisitOptionalImplicitConversion(rightOperand, targetType, useLegacyWarnings: UseLegacyWarnings(leftOperand, targetType), trackMembers: false, AssignmentKind.Assignment); TrackNullableStateForAssignment(rightOperand, targetType, leftSlot, rightResult, MakeSlot(rightOperand)); Join(ref this.State, ref leftState); TypeWithState resultType = GetNullCoalescingResultType(rightResult, targetType.Type); SetResultType(node, resultType); return null; } public override BoundNode? VisitNullCoalescingOperator(BoundNullCoalescingOperator node) { Debug.Assert(!IsConditionalState); BoundExpression leftOperand = node.LeftOperand; BoundExpression rightOperand = node.RightOperand; TypeWithState leftResult = VisitRvalueWithState(leftOperand); TypeWithState rightResult; if (IsConstantNull(leftOperand)) { rightResult = VisitRvalueWithState(rightOperand); // Should be able to use rightResult for the result of the operator but // binding may have generated a different result type in the case of errors. SetResultType(node, TypeWithState.Create(node.Type, rightResult.State)); return null; } var whenNotNull = this.State.Clone(); LearnFromNonNullTest(leftOperand, ref whenNotNull); LearnFromNullTest(leftOperand, ref this.State); bool leftIsConstant = leftOperand.ConstantValue != null; if (leftIsConstant) { SetUnreachable(); } // https://github.com/dotnet/roslyn/issues/29955 For cases where the left operand determines // the type, we should unwrap the right conversion and re-apply. rightResult = VisitRvalueWithState(rightOperand); Join(ref this.State, ref whenNotNull); if (rightOperand.ConstantValue?.IsBoolean ?? false) { Split(); if (rightOperand.ConstantValue.BooleanValue) { StateWhenFalse = whenNotNull; } else { StateWhenTrue = whenNotNull; } } var leftResultType = leftResult.Type; var rightResultType = rightResult.Type; var resultType = node.OperatorResultKind switch { BoundNullCoalescingOperatorResultKind.NoCommonType => node.Type, BoundNullCoalescingOperatorResultKind.LeftType => getLeftResultType(leftResultType!, rightResultType!), BoundNullCoalescingOperatorResultKind.LeftUnwrappedType => getLeftResultType(leftResultType!.StrippedType(), rightResultType!), BoundNullCoalescingOperatorResultKind.RightType => getRightResultType(leftResultType!, rightResultType!), BoundNullCoalescingOperatorResultKind.LeftUnwrappedRightType => getRightResultType(leftResultType!.StrippedType(), rightResultType!), BoundNullCoalescingOperatorResultKind.RightDynamicType => rightResultType!, _ => throw ExceptionUtilities.UnexpectedValue(node.OperatorResultKind), }; SetResultType(node, GetNullCoalescingResultType(rightResult, resultType)); return null; TypeSymbol getLeftResultType(TypeSymbol leftType, TypeSymbol rightType) { Debug.Assert(rightType is object); // If there was an identity conversion between the two operands (in short, if there // is no implicit conversion on the right operand), then check nullable conversions // in both directions since it's possible the right operand is the better result type. if ((node.RightOperand as BoundConversion)?.ExplicitCastInCode != false && GenerateConversionForConditionalOperator(node.LeftOperand, leftType, rightType, reportMismatch: false).Exists) { return rightType; } GenerateConversionForConditionalOperator(node.RightOperand, rightType, leftType, reportMismatch: true); return leftType; } TypeSymbol getRightResultType(TypeSymbol leftType, TypeSymbol rightType) { GenerateConversionForConditionalOperator(node.LeftOperand, leftType, rightType, reportMismatch: true); return rightType; } } private static TypeWithState GetNullCoalescingResultType(TypeWithState rightResult, TypeSymbol resultType) { NullableFlowState resultState = rightResult.State; return TypeWithState.Create(resultType, resultState); } public override BoundNode? VisitConditionalAccess(BoundConditionalAccess node) { Debug.Assert(!IsConditionalState); var receiver = node.Receiver; _ = VisitRvalueWithState(receiver); _currentConditionalReceiverVisitResult = _visitResult; var previousConditionalAccessSlot = _lastConditionalAccessSlot; var receiverState = this.State.Clone(); if (IsConstantNull(node.Receiver)) { SetUnreachable(); _lastConditionalAccessSlot = -1; } else { // In the when-null branch, the receiver is known to be maybe-null. // In the other branch, the receiver is known to be non-null. LearnFromNullTest(receiver, ref receiverState); LearnFromNonNullTest(receiver, ref this.State); var nextConditionalAccessSlot = MakeSlot(receiver); if (nextConditionalAccessSlot > 0 && receiver.Type?.IsNullableType() == true) nextConditionalAccessSlot = GetNullableOfTValueSlot(receiver.Type, nextConditionalAccessSlot, out _); _lastConditionalAccessSlot = nextConditionalAccessSlot; } var accessTypeWithAnnotations = VisitLvalueWithAnnotations(node.AccessExpression); TypeSymbol accessType = accessTypeWithAnnotations.Type; Join(ref this.State, ref receiverState); var oldType = node.Type; var resultType = oldType.IsVoidType() || oldType.IsErrorType() ? oldType : oldType.IsNullableType() && !accessType.IsNullableType() ? MakeNullableOf(accessTypeWithAnnotations) : accessType; // Per LDM 2019-02-13 decision, the result of a conditional access "may be null" even if // both the receiver and right-hand-side are believed not to be null. SetResultType(node, TypeWithState.Create(resultType, NullableFlowState.MaybeNull)); _currentConditionalReceiverVisitResult = default; _lastConditionalAccessSlot = previousConditionalAccessSlot; return null; } protected override BoundNode? VisitConditionalOperatorCore( BoundExpression node, bool isRef, BoundExpression condition, BoundExpression originalConsequence, BoundExpression originalAlternative) { VisitCondition(condition); var consequenceState = this.StateWhenTrue; var alternativeState = this.StateWhenFalse; TypeWithState consequenceRValue; TypeWithState alternativeRValue; if (isRef) { TypeWithAnnotations consequenceLValue; TypeWithAnnotations alternativeLValue; (consequenceLValue, consequenceRValue) = visitConditionalRefOperand(consequenceState, originalConsequence); consequenceState = this.State; (alternativeLValue, alternativeRValue) = visitConditionalRefOperand(alternativeState, originalAlternative); Join(ref this.State, ref consequenceState); TypeSymbol? refResultType = node.Type?.SetUnknownNullabilityForReferenceTypes(); if (IsNullabilityMismatch(consequenceLValue, alternativeLValue)) { // l-value types must match ReportNullabilityMismatchInAssignment(node.Syntax, consequenceLValue, alternativeLValue); } else if (!node.HasErrors) { refResultType = consequenceRValue.Type!.MergeEquivalentTypes(alternativeRValue.Type, VarianceKind.None); } var lValueAnnotation = consequenceLValue.NullableAnnotation.EnsureCompatible(alternativeLValue.NullableAnnotation); var rValueState = consequenceRValue.State.Join(alternativeRValue.State); SetResult(node, TypeWithState.Create(refResultType, rValueState), TypeWithAnnotations.Create(refResultType, lValueAnnotation)); return null; } BoundExpression consequence; BoundExpression alternative; Conversion consequenceConversion; Conversion alternativeConversion; bool consequenceEndReachable; bool alternativeEndReachable; // In cases where one branch is unreachable, we don't need to Unsplit the state if (!alternativeState.Reachable) { (alternative, alternativeConversion, alternativeRValue) = visitConditionalOperand(alternativeState, originalAlternative); (consequence, consequenceConversion, consequenceRValue) = visitConditionalOperand(consequenceState, originalConsequence); alternativeEndReachable = false; consequenceEndReachable = IsReachable(); } else if (!consequenceState.Reachable) { (consequence, consequenceConversion, consequenceRValue) = visitConditionalOperand(consequenceState, originalConsequence); (alternative, alternativeConversion, alternativeRValue) = visitConditionalOperand(alternativeState, originalAlternative); consequenceEndReachable = false; alternativeEndReachable = IsReachable(); } else { (consequence, consequenceConversion, consequenceRValue) = visitConditionalOperand(consequenceState, originalConsequence); Unsplit(); consequenceState = this.State; consequenceEndReachable = consequenceState.Reachable; (alternative, alternativeConversion, alternativeRValue) = visitConditionalOperand(alternativeState, originalAlternative); Unsplit(); alternativeEndReachable = this.State.Reachable; Join(ref this.State, ref consequenceState); } TypeSymbol? resultType; if (node.HasErrors || node is BoundConditionalOperator { WasTargetTyped: true }) { resultType = null; } else { // Determine nested nullability using BestTypeInferrer. // If a branch is unreachable, we could use the nested nullability of the other // branch, but that requires using the nullability of the branch as it applies to the // target type. For instance, the result of the conditional in the following should // be `IEnumerable` not `object[]`: // object[] a = ...; // IEnumerable b = ...; // var c = true ? a : b; BoundExpression consequencePlaceholder = CreatePlaceholderIfNecessary(consequence, consequenceRValue.ToTypeWithAnnotations(compilation)); BoundExpression alternativePlaceholder = CreatePlaceholderIfNecessary(alternative, alternativeRValue.ToTypeWithAnnotations(compilation)); HashSet? useSiteDiagnostics = null; resultType = BestTypeInferrer.InferBestTypeForConditionalOperator(consequencePlaceholder, alternativePlaceholder, _conversions, out _, ref useSiteDiagnostics); } NullableFlowState resultState; if (resultType is null) { resultType = node.Type?.SetUnknownNullabilityForReferenceTypes(); resultState = consequenceRValue.State.Join(alternativeRValue.State); var resultTypeWithState = TypeWithState.Create(resultType, resultState); if (consequence != originalConsequence) { TrackAnalyzedNullabilityThroughConversionGroup(resultTypeWithState, (BoundConversion)originalConsequence, consequence); } if (alternative != originalAlternative) { TrackAnalyzedNullabilityThroughConversionGroup(resultTypeWithState, (BoundConversion)originalAlternative, alternative); } } else { var resultTypeWithAnnotations = TypeWithAnnotations.Create(resultType); TypeWithState convertedConsequenceResult = convertResult( originalConsequence, consequence, consequenceConversion, resultTypeWithAnnotations, consequenceRValue, consequenceState, consequenceEndReachable); TypeWithState convertedAlternativeResult = convertResult( originalAlternative, alternative, alternativeConversion, resultTypeWithAnnotations, alternativeRValue, alternativeState, alternativeEndReachable); resultState = convertedConsequenceResult.State.Join(convertedAlternativeResult.State); } SetResultType(node, TypeWithState.Create(resultType, resultState)); return null; (BoundExpression, Conversion, TypeWithState) visitConditionalOperand(LocalState state, BoundExpression operand) { Conversion conversion; SetState(state); Debug.Assert(!isRef); BoundExpression operandNoConversion; (operandNoConversion, conversion) = RemoveConversion(operand, includeExplicitConversions: false); SnapshotWalkerThroughConversionGroup(operand, operandNoConversion); Visit(operandNoConversion); return (operandNoConversion, conversion, ResultType); } (TypeWithAnnotations LValueType, TypeWithState RValueType) visitConditionalRefOperand(LocalState state, BoundExpression operand) { SetState(state); Debug.Assert(isRef); TypeWithAnnotations lValueType = VisitLvalueWithAnnotations(operand); return (lValueType, ResultType); } TypeWithState convertResult( BoundExpression node, BoundExpression operand, Conversion conversion, TypeWithAnnotations targetType, TypeWithState operandType, LocalState state, bool isReachable) { var savedState = this.State; this.State = state; bool previousDisabledDiagnostics = _disableDiagnostics; // If the node is not reachable, then we're only visiting to get // nullability information for the public API, and not to produce diagnostics. // Disable diagnostics, and return default for the resulting state // to indicate that warnings were suppressed. if (!isReachable) { _disableDiagnostics = true; } var resultType = VisitConversion( GetConversionIfApplicable(node, operand), operand, conversion, targetType, operandType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportTopLevelWarnings: false); if (!isReachable) { resultType = default; _disableDiagnostics = previousDisabledDiagnostics; } this.State = savedState; return resultType; } } private bool IsReachable() => this.IsConditionalState ? (this.StateWhenTrue.Reachable || this.StateWhenFalse.Reachable) : this.State.Reachable; /// /// Placeholders are bound expressions with type and state. /// But for typeless expressions (such as `null` or `(null, null)` we hold onto the original bound expression, /// as it will be useful for conversions from expression. /// private static BoundExpression CreatePlaceholderIfNecessary(BoundExpression expr, TypeWithAnnotations type) { return !type.HasType ? expr : new BoundExpressionWithNullability(expr.Syntax, expr, type.NullableAnnotation, type.Type); } public override BoundNode? VisitConditionalReceiver(BoundConditionalReceiver node) { var rvalueType = _currentConditionalReceiverVisitResult.RValueType.Type; if (rvalueType?.IsNullableType() == true) { rvalueType = rvalueType.GetNullableUnderlyingType(); } SetResultType(node, TypeWithState.Create(rvalueType, NullableFlowState.NotNull)); return null; } public override BoundNode? VisitCall(BoundCall node) { // Note: we analyze even omitted calls TypeWithState receiverType = VisitCallReceiver(node); ReinferMethodAndVisitArguments(node, receiverType); return null; } private void ReinferMethodAndVisitArguments(BoundCall node, TypeWithState receiverType) { var method = node.Method; ImmutableArray refKindsOpt = node.ArgumentRefKindsOpt; if (!receiverType.HasNullType) { // Update method based on inferred receiver type. method = (MethodSymbol)AsMemberOfType(receiverType.Type, method); } ImmutableArray results; bool returnNotNull; (method, results, returnNotNull) = VisitArguments(node, node.Arguments, refKindsOpt, method!.Parameters, node.ArgsToParamsOpt, node.Expanded, node.InvokedAsExtensionMethod, method); ApplyMemberPostConditions(node.ReceiverOpt, method); LearnFromEqualsMethod(method, node, receiverType, results); var returnState = GetReturnTypeWithState(method); if (returnNotNull) { returnState = returnState.WithNotNullState(); } SetResult(node, returnState, method.ReturnTypeWithAnnotations); SetUpdatedSymbol(node, node.Method, method); } private void LearnFromEqualsMethod(MethodSymbol method, BoundCall node, TypeWithState receiverType, ImmutableArray results) { // easy out var parameterCount = method.ParameterCount; var arguments = node.Arguments; if (node.HasErrors || (parameterCount != 1 && parameterCount != 2) || parameterCount != arguments.Length || method.MethodKind != MethodKind.Ordinary || method.ReturnType.SpecialType != SpecialType.System_Boolean || (method.Name != SpecialMembers.GetDescriptor(SpecialMember.System_Object__Equals).Name && method.Name != SpecialMembers.GetDescriptor(SpecialMember.System_Object__ReferenceEquals).Name && !anyOverriddenMethodHasExplicitImplementation(method))) { return; } var isStaticEqualsMethod = method.Equals(compilation.GetSpecialTypeMember(SpecialMember.System_Object__EqualsObjectObject)) || method.Equals(compilation.GetSpecialTypeMember(SpecialMember.System_Object__ReferenceEquals)); if (isStaticEqualsMethod || isWellKnownEqualityMethodOrImplementation(compilation, method, receiverType.Type, WellKnownMember.System_Collections_Generic_IEqualityComparer_T__Equals)) { Debug.Assert(arguments.Length == 2); learnFromEqualsMethodArguments(arguments[0], results[0].RValueType, arguments[1], results[1].RValueType); return; } var isObjectEqualsMethodOrOverride = method.GetLeastOverriddenMethod(accessingTypeOpt: null) .Equals(compilation.GetSpecialTypeMember(SpecialMember.System_Object__Equals)); if (node.ReceiverOpt is BoundExpression receiver && (isObjectEqualsMethodOrOverride || isWellKnownEqualityMethodOrImplementation(compilation, method, receiverType.Type, WellKnownMember.System_IEquatable_T__Equals))) { Debug.Assert(arguments.Length == 1); learnFromEqualsMethodArguments(receiver, receiverType, arguments[0], results[0].RValueType); return; } static bool anyOverriddenMethodHasExplicitImplementation(MethodSymbol method) { for (var overriddenMethod = method; overriddenMethod is object; overriddenMethod = overriddenMethod.OverriddenMethod) { if (overriddenMethod.IsExplicitInterfaceImplementation) { return true; } } return false; } static bool isWellKnownEqualityMethodOrImplementation(CSharpCompilation compilation, MethodSymbol method, TypeSymbol? receiverType, WellKnownMember wellKnownMember) { var wellKnownMethod = (MethodSymbol?)compilation.GetWellKnownTypeMember(wellKnownMember); if (wellKnownMethod is null || receiverType is null) { return false; } var wellKnownType = wellKnownMethod.ContainingType; var parameterType = method.Parameters[0].TypeWithAnnotations; var constructedType = wellKnownType.Construct(ImmutableArray.Create(parameterType)); var constructedMethod = wellKnownMethod.AsMember(constructedType); // FindImplementationForInterfaceMember doesn't check if this method is itself the interface method we're looking for if (constructedMethod.Equals(method)) { return true; } // check whether 'method', when called on this receiver, is an implementation of 'constructedMethod'. for (var baseType = receiverType; baseType is object && method is object; baseType = baseType.BaseTypeNoUseSiteDiagnostics) { var implementationMethod = baseType.FindImplementationForInterfaceMember(constructedMethod); if (implementationMethod is null) { // we know no base type will implement this interface member either return false; } if (implementationMethod.ContainingType.IsInterface) { // this method cannot be called directly from source because an interface can only explicitly implement a method from its base interface. return false; } // could be calling an override of a method that implements the interface method for (var overriddenMethod = method; overriddenMethod is object; overriddenMethod = overriddenMethod.OverriddenMethod) { if (overriddenMethod.Equals(implementationMethod)) { return true; } } // the Equals method being called isn't the method that implements the interface method in this type. // it could be a method that implements the interface on a base type, so check again with the base type of 'implementationMethod.ContainingType' // e.g. in this hierarchy: // class A -> B -> C -> D // method virtual B.Equals -> override D.Equals // // we would potentially check: // 1. D.Equals when called on D, then B.Equals when called on D // 2. B.Equals when called on C // 3. B.Equals when called on B // 4. give up when checking A, since B.Equals is not overriding anything in A // we know that implementationMethod.ContainingType is the same type or a base type of 'baseType', // and that the implementation method will be the same between 'baseType' and 'implementationMethod.ContainingType'. // we step through the intermediate bases in order to skip unnecessary override methods. while (!baseType.Equals(implementationMethod.ContainingType) && method is object) { if (baseType.Equals(method.ContainingType)) { // since we're about to move on to the base of 'method.ContainingType', // we know the implementation could only be an overridden method of 'method'. method = method.OverriddenMethod; } baseType = baseType.BaseTypeNoUseSiteDiagnostics; // the implementation method must be contained in this 'baseType' or one of its bases. Debug.Assert(baseType is object); } // now 'baseType == implementationMethod.ContainingType', so if 'method' is // contained in that same type we should advance 'method' one more time. if (method is object && baseType.Equals(method.ContainingType)) { method = method.OverriddenMethod; } } return false; } void learnFromEqualsMethodArguments(BoundExpression left, TypeWithState leftType, BoundExpression right, TypeWithState rightType) { // comparing anything to a null literal gives maybe-null when true and not-null when false // comparing a maybe-null to a not-null gives us not-null when true, nothing learned when false if (left.ConstantValue?.IsNull == true) { Split(); LearnFromNullTest(right, ref StateWhenTrue); LearnFromNonNullTest(right, ref StateWhenFalse); } else if (right.ConstantValue?.IsNull == true) { Split(); LearnFromNullTest(left, ref StateWhenTrue); LearnFromNonNullTest(left, ref StateWhenFalse); } else if (leftType.MayBeNull && rightType.IsNotNull) { Split(); LearnFromNonNullTest(left, ref StateWhenTrue); } else if (rightType.MayBeNull && leftType.IsNotNull) { Split(); LearnFromNonNullTest(right, ref StateWhenTrue); } } } private bool IsCompareExchangeMethod(MethodSymbol? method) { if (method is null) { return false; } return method.Equals(compilation.GetWellKnownTypeMember(WellKnownMember.System_Threading_Interlocked__CompareExchange), SymbolEqualityComparer.ConsiderEverything.CompareKind) || method.OriginalDefinition.Equals(compilation.GetWellKnownTypeMember(WellKnownMember.System_Threading_Interlocked__CompareExchange_T), SymbolEqualityComparer.ConsiderEverything.CompareKind); } private readonly struct CompareExchangeInfo { public readonly ImmutableArray Arguments; public readonly ImmutableArray Results; public readonly ImmutableArray ArgsToParamsOpt; public CompareExchangeInfo(ImmutableArray arguments, ImmutableArray results, ImmutableArray argsToParamsOpt) { Arguments = arguments; Results = results; ArgsToParamsOpt = argsToParamsOpt; } public bool IsDefault => Arguments.IsDefault || Results.IsDefault; } private NullableFlowState LearnFromCompareExchangeMethod(in CompareExchangeInfo compareExchangeInfo) { Debug.Assert(!compareExchangeInfo.IsDefault); // In general a call to CompareExchange of the form: // // Interlocked.CompareExchange(ref location, value, comparand); // // will be analyzed similarly to the following: // // if (location == comparand) // { // location = value; // } if (compareExchangeInfo.Arguments.Length != 3) { // This can occur if CompareExchange has optional arguments. // Since none of the main runtimes have optional arguments, // we bail to avoid an exception but don't bother actually calculating the FlowState. return NullableFlowState.NotNull; } var argsToParamsOpt = compareExchangeInfo.ArgsToParamsOpt; Debug.Assert(argsToParamsOpt is { IsDefault: true } or { Length: 3 }); var (comparandIndex, valueIndex, locationIndex) = argsToParamsOpt.IsDefault ? (2, 1, 0) : (argsToParamsOpt.IndexOf(2), argsToParamsOpt.IndexOf(1), argsToParamsOpt.IndexOf(0)); var comparand = compareExchangeInfo.Arguments[comparandIndex]; var valueFlowState = compareExchangeInfo.Results[valueIndex].RValueType.State; if (comparand.ConstantValue?.IsNull == true) { // If location contained a null, then the write `location = value` definitely occurred } else { var locationFlowState = compareExchangeInfo.Results[locationIndex].RValueType.State; // A write may have occurred valueFlowState = valueFlowState.Join(locationFlowState); } return valueFlowState; } private TypeWithState VisitCallReceiver(BoundCall node) { var receiverOpt = node.ReceiverOpt; TypeWithState receiverType = default; if (receiverOpt != null) { receiverType = VisitRvalueWithState(receiverOpt); // methods which are members of Nullable (ex: ToString, GetHashCode) can be invoked on null receiver. // However, inherited methods (ex: GetType) are invoked on a boxed value (since base types are reference types) // and therefore in those cases nullable receivers should be checked for nullness. bool checkNullableValueType = false; var type = receiverType.Type; var method = node.Method; if (method.RequiresInstanceReceiver && type?.IsNullableType() == true && method.ContainingType.IsReferenceType) { checkNullableValueType = true; } else if (method.OriginalDefinition == compilation.GetSpecialTypeMember(SpecialMember.System_Nullable_T_get_Value)) { // call to get_Value may not occur directly in source, but may be inserted as a result of premature lowering. // One example where we do it is foreach with nullables. // The reason is Dev10 compatibility (see: UnwrapCollectionExpressionIfNullable in ForEachLoopBinder.cs) // Regardless of the reasons, we know that the method does not tolerate nulls. checkNullableValueType = true; } // https://github.com/dotnet/roslyn/issues/30598: Mark receiver as not null // after arguments have been visited, and only if the receiver has not changed. _ = CheckPossibleNullReceiver(receiverOpt, checkNullableValueType); } return receiverType; } private TypeWithState GetReturnTypeWithState(MethodSymbol method) { return TypeWithState.Create(method.ReturnTypeWithAnnotations, GetRValueAnnotations(method)); } private FlowAnalysisAnnotations GetRValueAnnotations(Symbol? symbol) { // Annotations are ignored when binding an attribute to avoid cycles. (Members used // in attributes are error scenarios, so missing warnings should not be important.) if (IsAnalyzingAttribute) { return FlowAnalysisAnnotations.None; } var annotations = symbol.GetFlowAnalysisAnnotations(); return annotations & (FlowAnalysisAnnotations.MaybeNull | FlowAnalysisAnnotations.NotNull); } private FlowAnalysisAnnotations GetParameterAnnotations(ParameterSymbol parameter) { // Annotations are ignored when binding an attribute to avoid cycles. (Members used // in attributes are error scenarios, so missing warnings should not be important.) return IsAnalyzingAttribute ? FlowAnalysisAnnotations.None : parameter.FlowAnalysisAnnotations; } /// /// Fix a TypeWithAnnotations based on Allow/DisallowNull annotations prior to a conversion or assignment. /// Note this does not work for nullable value types, so an additional check with may be required. /// private static TypeWithAnnotations ApplyLValueAnnotations(TypeWithAnnotations declaredType, FlowAnalysisAnnotations flowAnalysisAnnotations) { if ((flowAnalysisAnnotations & FlowAnalysisAnnotations.DisallowNull) == FlowAnalysisAnnotations.DisallowNull) { return declaredType.AsNotAnnotated(); } else if ((flowAnalysisAnnotations & FlowAnalysisAnnotations.AllowNull) == FlowAnalysisAnnotations.AllowNull) { return declaredType.AsAnnotated(); } return declaredType; } /// /// Update the null-state based on MaybeNull/NotNull /// private static TypeWithState ApplyUnconditionalAnnotations(TypeWithState typeWithState, FlowAnalysisAnnotations annotations) { if ((annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull) { return TypeWithState.Create(typeWithState.Type, NullableFlowState.NotNull); } if ((annotations & FlowAnalysisAnnotations.MaybeNull) == FlowAnalysisAnnotations.MaybeNull) { return TypeWithState.Create(typeWithState.Type, NullableFlowState.MaybeDefault); } return typeWithState; } private static TypeWithAnnotations ApplyUnconditionalAnnotations(TypeWithAnnotations declaredType, FlowAnalysisAnnotations annotations) { if ((annotations & FlowAnalysisAnnotations.MaybeNull) == FlowAnalysisAnnotations.MaybeNull) { return declaredType.AsAnnotated(); } if ((annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull) { return declaredType.AsNotAnnotated(); } return declaredType; } // https://github.com/dotnet/roslyn/issues/29863 Record in the node whether type // arguments were implicit, to allow for cases where the syntax is not an // invocation (such as a synthesized call from a query interpretation). private static bool HasImplicitTypeArguments(BoundNode node) { if (node is BoundCollectionElementInitializer { AddMethod: { TypeArgumentsWithAnnotations: { IsEmpty: false } } }) { return true; } if (node is BoundForEachStatement { EnumeratorInfoOpt: { GetEnumeratorMethod: { TypeArgumentsWithAnnotations: { IsEmpty: false } } } }) { return true; } var syntax = node.Syntax; if (syntax.Kind() != SyntaxKind.InvocationExpression) { // Unexpected syntax kind. return false; } return HasImplicitTypeArguments(((InvocationExpressionSyntax)syntax).Expression); } private static bool HasImplicitTypeArguments(SyntaxNode syntax) { var nameSyntax = Binder.GetNameSyntax(syntax, out _); if (nameSyntax == null) { // Unexpected syntax kind. return false; } nameSyntax = nameSyntax.GetUnqualifiedName(); return nameSyntax.Kind() != SyntaxKind.GenericName; } protected override void VisitArguments(ImmutableArray arguments, ImmutableArray refKindsOpt, MethodSymbol method) { // Callers should be using VisitArguments overload below. throw ExceptionUtilities.Unreachable; } private (MethodSymbol? method, ImmutableArray results, bool returnNotNull) VisitArguments( BoundExpression node, ImmutableArray arguments, ImmutableArray refKindsOpt, MethodSymbol? method, ImmutableArray argsToParamsOpt, bool expanded, bool invokedAsExtensionMethod) { return VisitArguments(node, arguments, refKindsOpt, method is null ? default : method.Parameters, argsToParamsOpt, expanded, invokedAsExtensionMethod, method); } private ImmutableArray VisitArguments( BoundExpression node, ImmutableArray arguments, ImmutableArray refKindsOpt, PropertySymbol? property, ImmutableArray argsToParamsOpt, bool expanded) { return VisitArguments(node, arguments, refKindsOpt, property is null ? default : property.Parameters, argsToParamsOpt, expanded, invokedAsExtensionMethod: false).results; } /// /// If you pass in a method symbol, its type arguments will be re-inferred and the re-inferred method will be returned. /// private (MethodSymbol? method, ImmutableArray results, bool returnNotNull) VisitArguments( BoundNode node, ImmutableArray arguments, ImmutableArray refKindsOpt, ImmutableArray parametersOpt, ImmutableArray argsToParamsOpt, bool expanded, bool invokedAsExtensionMethod, MethodSymbol? method = null) { Debug.Assert(!arguments.IsDefault); bool shouldReturnNotNull = false; (ImmutableArray argumentsNoConversions, ImmutableArray conversions) = RemoveArgumentConversions(arguments, refKindsOpt); // Visit the arguments and collect results ImmutableArray results; (results, argumentsNoConversions, argsToParamsOpt, refKindsOpt) = VisitArgumentsEvaluate(node.Syntax, argumentsNoConversions, refKindsOpt, parametersOpt, argsToParamsOpt, expanded); // Re-infer method type parameters if (method?.IsGenericMethod == true) { if (HasImplicitTypeArguments(node)) { var binder = node switch { BoundCall { BinderOpt: { } b } => b, BoundCollectionElementInitializer { BinderOpt: { } b } => b, BoundForEachStatement { EnumeratorInfoOpt: { Binder: { } b } } => b, _ => throw ExceptionUtilities.UnexpectedValue(node) }; method = InferMethodTypeArguments(binder, method, GetArgumentsForMethodTypeInference(results, argumentsNoConversions), refKindsOpt, argsToParamsOpt, expanded); parametersOpt = method.Parameters; } if (ConstraintsHelper.RequiresChecking(method)) { var syntax = node.Syntax; CheckMethodConstraints( syntax switch { InvocationExpressionSyntax { Expression: var expression } => expression, ForEachStatementSyntax { Expression: var expression } => expression, _ => syntax }, method); } } bool parameterHasNotNullIfNotNull = !IsAnalyzingAttribute && !parametersOpt.IsDefault && parametersOpt.Any(p => !p.NotNullIfParameterNotNull.IsEmpty); var notNullParametersBuilder = parameterHasNotNullIfNotNull ? ArrayBuilder.GetInstance() : null; if (!node.HasErrors && !parametersOpt.IsDefault) { // Visit conversions, inbound assignments including pre-conditions ImmutableHashSet? returnNotNullIfParameterNotNull = IsAnalyzingAttribute ? null : method?.ReturnNotNullIfParameterNotNull; for (int i = 0; i < results.Length; i++) { (ParameterSymbol? parameter, TypeWithAnnotations parameterType, FlowAnalysisAnnotations parameterAnnotations, bool isExpandedParamsArgument) = GetCorrespondingParameter(i, parametersOpt, argsToParamsOpt, expanded); if (parameter is null) { continue; } var argumentNoConversion = argumentsNoConversions[i]; var argument = i < arguments.Length ? arguments[i] : argumentsNoConversions[i]; VisitArgumentConversionAndInboundAssignmentsAndPreConditions( GetConversionIfApplicable(argument, argumentNoConversion), argumentNoConversion, conversions.IsDefault || i >= conversions.Length ? Conversion.Identity : conversions[i], GetRefKind(refKindsOpt, i), parameter, parameterType, parameterAnnotations, results[i], invokedAsExtensionMethod && i == 0); if (results[i].RValueType.IsNotNull || isExpandedParamsArgument) { notNullParametersBuilder?.Add(parameter); if (returnNotNullIfParameterNotNull?.Contains(parameter.Name) == true) { shouldReturnNotNull = true; } } } } else { // Normally we delay visiting the lambda until we can visit it along with its conversion. // Since we can't visit its conversion here, or it doesn't have one, we dig back in and // visit the lambda here to ensure all nodes have nullability info for public API for (int i = 0; i < results.Length; i++) { if (argumentsNoConversions[i] is BoundLambda lambda) { VisitLambda(lambda, delegateTypeOpt: null, results[i].StateForLambda); } } } if (node is BoundCall { Method: { OriginalDefinition: LocalFunctionSymbol localFunction } }) { VisitLocalFunctionUse(localFunction); } if (!node.HasErrors && !parametersOpt.IsDefault) { // For CompareExchange method we need more context to determine the state of outbound assignment CompareExchangeInfo compareExchangeInfo = IsCompareExchangeMethod(method) ? new CompareExchangeInfo(arguments, results, argsToParamsOpt) : default; // Visit outbound assignments and post-conditions // Note: the state may get split in this step for (int i = 0; i < arguments.Length; i++) { (ParameterSymbol? parameter, TypeWithAnnotations parameterType, FlowAnalysisAnnotations parameterAnnotations, _) = GetCorrespondingParameter(i, parametersOpt, argsToParamsOpt, expanded); if (parameter is null) { continue; } VisitArgumentOutboundAssignmentsAndPostConditions( arguments[i], GetRefKind(refKindsOpt, i), parameter, parameterType, parameterAnnotations, results[i], notNullParametersBuilder, (!compareExchangeInfo.IsDefault && parameter.Ordinal == 0) ? compareExchangeInfo : default); } } else { for (int i = 0; i < arguments.Length; i++) { // We can hit this case when dynamic methods are involved, or when there are errors. In either case we have no information, // so just assume that the conversions have the same nullability as the underlying result var argument = arguments[i]; var result = results[i]; var argumentNoConversion = argumentsNoConversions[i]; TrackAnalyzedNullabilityThroughConversionGroup(TypeWithState.Create(argument.Type, result.RValueType.State), argument as BoundConversion, argumentNoConversion); } } if (!IsAnalyzingAttribute && method is object && (method.FlowAnalysisAnnotations & FlowAnalysisAnnotations.DoesNotReturn) == FlowAnalysisAnnotations.DoesNotReturn) { SetUnreachable(); } notNullParametersBuilder?.Free(); return (method, results, shouldReturnNotNull); } private void ApplyMemberPostConditions(BoundExpression? receiverOpt, MethodSymbol? method) { if (method is null) { return; } int receiverSlot = method.IsStatic ? 0 : receiverOpt is null ? -1 : MakeSlot(receiverOpt); if (receiverSlot < 0) { return; } do { var type = method.ContainingType; var notNullMembers = method.NotNullMembers; var notNullWhenTrueMembers = method.NotNullWhenTrueMembers; var notNullWhenFalseMembers = method.NotNullWhenFalseMembers; if (IsConditionalState) { applyMemberPostConditions(receiverSlot, type, notNullMembers, ref StateWhenTrue); applyMemberPostConditions(receiverSlot, type, notNullMembers, ref StateWhenFalse); } else { applyMemberPostConditions(receiverSlot, type, notNullMembers, ref State); } if (!notNullWhenTrueMembers.IsEmpty || !notNullWhenFalseMembers.IsEmpty) { Split(); applyMemberPostConditions(receiverSlot, type, notNullWhenTrueMembers, ref StateWhenTrue); applyMemberPostConditions(receiverSlot, type, notNullWhenFalseMembers, ref StateWhenFalse); } method = method.OverriddenMethod; } while (method != null); void applyMemberPostConditions(int receiverSlot, TypeSymbol type, ImmutableArray members, ref LocalState state) { if (members.IsEmpty) { return; } foreach (var memberName in members) { markMembersAsNotNull(receiverSlot, type, memberName, ref state); } } void markMembersAsNotNull(int receiverSlot, TypeSymbol type, string memberName, ref LocalState state) { foreach (Symbol member in type.GetMembers(memberName)) { if (member.IsStatic) { receiverSlot = 0; } switch (member.Kind) { case SymbolKind.Field: case SymbolKind.Property: if (GetOrCreateSlot(member, receiverSlot) is int memberSlot && memberSlot > 0) { state[memberSlot] = NullableFlowState.NotNull; } break; case SymbolKind.Event: case SymbolKind.Method: break; } } } } private (ImmutableArray results, ImmutableArray arguments, ImmutableArray allArgsToParamsOpt, ImmutableArray allRefKindsOpt) VisitArgumentsEvaluate( SyntaxNode syntax, ImmutableArray arguments, ImmutableArray refKindsOpt, ImmutableArray parametersOpt, ImmutableArray argsToParamsOpt, bool expanded) { Debug.Assert(!IsConditionalState); int n = arguments.Length; if (n == 0 && parametersOpt.IsDefaultOrEmpty) { return (ImmutableArray.Empty, arguments, argsToParamsOpt, refKindsOpt); } var visitedParameters = PooledHashSet.GetInstance(); var resultsBuilder = ArrayBuilder.GetInstance(n); for (int i = 0; i < n; i++) { var (parameter, _, parameterAnnotations, _) = GetCorrespondingParameter(i, parametersOpt, argsToParamsOpt, expanded); resultsBuilder.Add(VisitArgumentEvaluate(arguments[i], GetRefKind(refKindsOpt, i), parameterAnnotations)); visitedParameters.Add(parameter); } if (!parametersOpt.IsDefaultOrEmpty && parametersOpt.Length != visitedParameters.Count) { var argumentsBuilder = initBuilder(arguments)!; var argsToParamsBuilder = initBuilder(argsToParamsOpt); var argRefKindsBuilder = initBuilder(refKindsOpt); var previousDisableNullabilityAnalysis = _disableNullabilityAnalysis; // Synthesized default arguments will not be found in the DebugVerifier's traversal of the bound tree. // Therefore we need to ensure they don't get added to _analyzedNullabilityMapOpt. _disableNullabilityAnalysis = true; for (int i = 0; i < parametersOpt.Length; i++) { var parameter = parametersOpt[i]; // Fill in unspecified optional arguments // Note that the order of visiting the optional arguments doesn't matter because they are constants. if (parameter.IsOptional && !visitedParameters.Contains(parameter)) { var annotations = GetParameterAnnotations(parameter); BoundExpression argument = GetDefaultParameterValue(syntax, parameter); resultsBuilder.Add(VisitArgumentEvaluate(argument, RefKind.None, annotations)); argumentsBuilder.Add(argument); argsToParamsBuilder?.Add(i); argRefKindsBuilder?.Add(RefKind.None); } } _disableNullabilityAnalysis = previousDisableNullabilityAnalysis; arguments = argumentsBuilder.ToImmutableAndFree(); argsToParamsOpt = argsToParamsBuilder?.ToImmutableAndFree() ?? default; refKindsOpt = argRefKindsBuilder?.ToImmutableAndFree() ?? default; } SetInvalidResult(); visitedParameters.Free(); return (resultsBuilder.ToImmutableAndFree(), arguments, argsToParamsOpt, refKindsOpt); ArrayBuilder? initBuilder(ImmutableArray arrayOpt) { if (arrayOpt.IsDefault) { return null; } var builder = ArrayBuilder.GetInstance(parametersOpt.Length); builder.AddRange(arrayOpt); return builder; } } private BoundExpression GetDefaultParameterValue(SyntaxNode syntax, ParameterSymbol parameter) { _defaultValuesOpt ??= PooledDictionary<(SyntaxNode, ParameterSymbol), BoundExpression>.GetInstance(); if (!_defaultValuesOpt.TryGetValue((syntax, parameter), out var argument)) { _defaultValuesOpt[(syntax, parameter)] = argument = LocalRewriter.GetDefaultParameterValue(syntax, parameter, enableCallerInfo: ThreeState.True, localRewriter: null, _binder, Diagnostics); } return argument; } private VisitArgumentResult VisitArgumentEvaluate(BoundExpression argument, RefKind refKind, FlowAnalysisAnnotations annotations) { Debug.Assert(!IsConditionalState); var savedState = (argument.Kind == BoundKind.Lambda) ? this.State.Clone() : default(Optional); // Note: DoesNotReturnIf is ineffective on ref/out parameters switch (refKind) { case RefKind.Ref: Visit(argument); Unsplit(); break; case RefKind.None: case RefKind.In: switch (annotations & (FlowAnalysisAnnotations.DoesNotReturnIfTrue | FlowAnalysisAnnotations.DoesNotReturnIfFalse)) { case FlowAnalysisAnnotations.DoesNotReturnIfTrue: Visit(argument); if (IsConditionalState) { SetState(StateWhenFalse); } break; case FlowAnalysisAnnotations.DoesNotReturnIfFalse: Visit(argument); if (IsConditionalState) { SetState(StateWhenTrue); } break; default: VisitRvalue(argument); break; } break; case RefKind.Out: // As far as we can tell, there is no scenario relevant to nullability analysis // where splitting an L-value (for instance with a ref conditional) would affect the result. Visit(argument); // We'll want to use the l-value type, rather than the result type, for method re-inference UseLvalueOnly(argument); break; } Debug.Assert(!IsConditionalState); return new VisitArgumentResult(_visitResult, savedState); } /// /// Verifies that an argument's nullability is compatible with its parameter's on the way in. /// private void VisitArgumentConversionAndInboundAssignmentsAndPreConditions( BoundConversion? conversionOpt, BoundExpression argumentNoConversion, Conversion conversion, RefKind refKind, ParameterSymbol parameter, TypeWithAnnotations parameterType, FlowAnalysisAnnotations parameterAnnotations, VisitArgumentResult result, bool extensionMethodThisArgument) { Debug.Assert(!this.IsConditionalState); // Note: we allow for some variance in `in` and `out` cases. Unlike in binding, we're not // limited by CLR constraints. var resultType = result.RValueType; switch (refKind) { case RefKind.None: case RefKind.In: { // Note: for lambda arguments, they will be converted in the context/state we saved for that argument var stateAfterConversion = VisitConversion( conversionOpt: conversionOpt, conversionOperand: argumentNoConversion, conversion: conversion, targetTypeWithNullability: ApplyLValueAnnotations(parameterType, parameterAnnotations), operandType: resultType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, assignmentKind: AssignmentKind.Argument, parameterOpt: parameter, extensionMethodThisArgument: extensionMethodThisArgument, stateForLambda: result.StateForLambda); // If the parameter has annotations, we perform an additional check for nullable value types CheckDisallowedNullAssignment(stateAfterConversion, parameterAnnotations, argumentNoConversion.Syntax.Location); SetResultType(argumentNoConversion, stateAfterConversion, updateAnalyzedNullability: false); } break; case RefKind.Ref: if (!argumentNoConversion.IsSuppressed) { var lvalueResultType = result.LValueType; if (IsNullabilityMismatch(lvalueResultType.Type, parameterType.Type)) { // declared types must match, ignoring top-level nullability ReportNullabilityMismatchInRefArgument(argumentNoConversion, argumentType: lvalueResultType.Type, parameter, parameterType.Type); } else { // types match, but state would let a null in ReportNullableAssignmentIfNecessary(argumentNoConversion, ApplyLValueAnnotations(parameterType, parameterAnnotations), resultType, useLegacyWarnings: false); // If the parameter has annotations, we perform an additional check for nullable value types CheckDisallowedNullAssignment(resultType, parameterAnnotations, argumentNoConversion.Syntax.Location); } } break; case RefKind.Out: break; default: throw ExceptionUtilities.UnexpectedValue(refKind); } Debug.Assert(!this.IsConditionalState); } private void CheckDisallowedNullAssignment(TypeWithState state, FlowAnalysisAnnotations annotations, Location location, BoundExpression? boundValueOpt = null) { if (boundValueOpt is { WasCompilerGenerated: true }) { // We need to skip `return backingField;` in auto-prop getters return; } // We do this extra check for types whose non-nullable version cannot be represented if (IsDisallowedNullAssignment(state, annotations)) { ReportDiagnostic(ErrorCode.WRN_DisallowNullAttributeForbidsMaybeNullAssignment, location); } } private static bool IsDisallowedNullAssignment(TypeWithState valueState, FlowAnalysisAnnotations targetAnnotations) { return ((targetAnnotations & FlowAnalysisAnnotations.DisallowNull) != 0) && hasNoNonNullableCounterpart(valueState.Type) && valueState.MayBeNull; static bool hasNoNonNullableCounterpart(TypeSymbol? type) { if (type is null) { return false; } // Some types that could receive a maybe-null value have a NotNull counterpart: // [NotNull]string? -> string // [NotNull]string -> string // [NotNull]TClass -> TClass // [NotNull]TClass? -> TClass // // While others don't: // [NotNull]int? -> X // [NotNull]TNullable -> X // [NotNull]TStruct? -> X // [NotNull]TOpen -> X return (type.Kind == SymbolKind.TypeParameter && !type.IsReferenceType) || type.IsNullableTypeOrTypeParameter(); } } /// /// Verifies that outbound assignments (from parameter to argument) are safe and /// tracks those assignments (or learns from post-condition attributes) /// private void VisitArgumentOutboundAssignmentsAndPostConditions( BoundExpression argument, RefKind refKind, ParameterSymbol parameter, TypeWithAnnotations parameterType, FlowAnalysisAnnotations parameterAnnotations, VisitArgumentResult result, ArrayBuilder? notNullParametersOpt, CompareExchangeInfo compareExchangeInfoOpt) { // Note: the state may be conditional if a previous argument involved a conditional post-condition // The WhenTrue/False states correspond to the invocation returning true/false switch (refKind) { case RefKind.None: case RefKind.In: { // learn from post-conditions [Maybe/NotNull, Maybe/NotNullWhen] without using an assignment learnFromPostConditions(argument, parameterType, parameterAnnotations); } break; case RefKind.Ref: { // assign from a fictional value from the parameter to the argument. parameterAnnotations = notNullBasedOnParameters(parameterAnnotations, notNullParametersOpt, parameter); var parameterWithState = TypeWithState.Create(parameterType, parameterAnnotations); if (!compareExchangeInfoOpt.IsDefault) { var adjustedState = LearnFromCompareExchangeMethod(in compareExchangeInfoOpt); parameterWithState = TypeWithState.Create(parameterType.Type, adjustedState); } var parameterValue = new BoundParameter(argument.Syntax, parameter); var lValueType = result.LValueType; trackNullableStateForAssignment(parameterValue, lValueType, MakeSlot(argument), parameterWithState, argument.IsSuppressed, parameterAnnotations); // check whether parameter would unsafely let a null out in the worse case if (!argument.IsSuppressed) { var leftAnnotations = GetLValueAnnotations(argument); ReportNullableAssignmentIfNecessary( parameterValue, targetType: ApplyLValueAnnotations(lValueType, leftAnnotations), valueType: applyPostConditionsUnconditionally(parameterWithState, parameterAnnotations), UseLegacyWarnings(argument, result.LValueType)); } } break; case RefKind.Out: { // compute the fictional parameter state parameterAnnotations = notNullBasedOnParameters(parameterAnnotations, notNullParametersOpt, parameter); var parameterWithState = TypeWithState.Create(parameterType, parameterAnnotations); // Adjust parameter state if MaybeNull or MaybeNullWhen are present (for `var` type and for assignment warnings) var worstCaseParameterWithState = applyPostConditionsUnconditionally(parameterWithState, parameterAnnotations); var declaredType = result.LValueType; var leftAnnotations = GetLValueAnnotations(argument); var lValueType = ApplyLValueAnnotations(declaredType, leftAnnotations); if (argument is BoundLocal local && local.DeclarationKind == BoundLocalDeclarationKind.WithInferredType) { var varType = worstCaseParameterWithState.ToAnnotatedTypeWithAnnotations(compilation); _variableTypes[local.LocalSymbol] = varType; lValueType = varType; } else if (argument is BoundDiscardExpression discard) { SetAnalyzedNullability(discard, new VisitResult(parameterWithState, parameterWithState.ToTypeWithAnnotations(compilation)), isLvalue: true); } // track state by assigning from a fictional value from the parameter to the argument. var parameterValue = new BoundParameter(argument.Syntax, parameter); // If the argument type has annotations, we perform an additional check for nullable value types CheckDisallowedNullAssignment(parameterWithState, leftAnnotations, argument.Syntax.Location); AdjustSetValue(argument, declaredType, lValueType, ref parameterWithState); trackNullableStateForAssignment(parameterValue, lValueType, MakeSlot(argument), parameterWithState, argument.IsSuppressed, parameterAnnotations); // report warnings if parameter would unsafely let a null out in the worst case if (!argument.IsSuppressed) { ReportNullableAssignmentIfNecessary(parameterValue, lValueType, worstCaseParameterWithState, UseLegacyWarnings(argument, result.LValueType)); HashSet? useSiteDiagnostics = null; if (!_conversions.HasIdentityOrImplicitReferenceConversion(parameterType.Type, lValueType.Type, ref useSiteDiagnostics)) { ReportNullabilityMismatchInArgument(argument.Syntax, lValueType.Type, parameter, parameterType.Type, forOutput: true); } } } break; default: throw ExceptionUtilities.UnexpectedValue(refKind); } FlowAnalysisAnnotations notNullBasedOnParameters(FlowAnalysisAnnotations parameterAnnotations, ArrayBuilder? notNullParametersOpt, ParameterSymbol parameter) { if (!IsAnalyzingAttribute && notNullParametersOpt is object) { var notNullIfParameterNotNull = parameter.NotNullIfParameterNotNull; if (!notNullIfParameterNotNull.IsEmpty) { foreach (var notNullParameter in notNullParametersOpt) { if (notNullIfParameterNotNull.Contains(notNullParameter.Name)) { return FlowAnalysisAnnotations.NotNull; } } } } return parameterAnnotations; } void trackNullableStateForAssignment(BoundExpression parameterValue, TypeWithAnnotations lValueType, int targetSlot, TypeWithState parameterWithState, bool isSuppressed, FlowAnalysisAnnotations parameterAnnotations) { if (!IsConditionalState && !hasConditionalPostCondition(parameterAnnotations)) { TrackNullableStateForAssignment(parameterValue, lValueType, targetSlot, parameterWithState.WithSuppression(isSuppressed)); } else { Split(); var originalWhenFalse = StateWhenFalse.Clone(); SetState(StateWhenTrue); // Note: the suppression applies over the post-condition attributes TrackNullableStateForAssignment(parameterValue, lValueType, targetSlot, applyPostConditionsWhenTrue(parameterWithState, parameterAnnotations).WithSuppression(isSuppressed)); Debug.Assert(!IsConditionalState); var newWhenTrue = State.Clone(); SetState(originalWhenFalse); TrackNullableStateForAssignment(parameterValue, lValueType, targetSlot, applyPostConditionsWhenFalse(parameterWithState, parameterAnnotations).WithSuppression(isSuppressed)); Debug.Assert(!IsConditionalState); SetConditionalState(newWhenTrue, whenFalse: State); } } static bool hasConditionalPostCondition(FlowAnalysisAnnotations annotations) { return (((annotations & FlowAnalysisAnnotations.MaybeNullWhenTrue) != 0) ^ ((annotations & FlowAnalysisAnnotations.MaybeNullWhenFalse) != 0)) || (((annotations & FlowAnalysisAnnotations.NotNullWhenTrue) != 0) ^ ((annotations & FlowAnalysisAnnotations.NotNullWhenFalse) != 0)); } static TypeWithState applyPostConditionsUnconditionally(TypeWithState typeWithState, FlowAnalysisAnnotations annotations) { if ((annotations & FlowAnalysisAnnotations.MaybeNull) != 0) { // MaybeNull and MaybeNullWhen return TypeWithState.Create(typeWithState.Type, NullableFlowState.MaybeDefault); } if ((annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull) { // NotNull return TypeWithState.Create(typeWithState.Type, NullableFlowState.NotNull); } return typeWithState; } static TypeWithState applyPostConditionsWhenTrue(TypeWithState typeWithState, FlowAnalysisAnnotations annotations) { bool notNullWhenTrue = (annotations & FlowAnalysisAnnotations.NotNullWhenTrue) != 0; bool maybeNullWhenTrue = (annotations & FlowAnalysisAnnotations.MaybeNullWhenTrue) != 0; bool maybeNullWhenFalse = (annotations & FlowAnalysisAnnotations.MaybeNullWhenFalse) != 0; if (maybeNullWhenTrue && !(maybeNullWhenFalse && notNullWhenTrue)) { // [MaybeNull, NotNullWhen(true)] means [MaybeNullWhen(false)] return TypeWithState.Create(typeWithState.Type, NullableFlowState.MaybeDefault); } else if (notNullWhenTrue) { return TypeWithState.Create(typeWithState.Type, NullableFlowState.NotNull); } return typeWithState; } static TypeWithState applyPostConditionsWhenFalse(TypeWithState typeWithState, FlowAnalysisAnnotations annotations) { bool notNullWhenFalse = (annotations & FlowAnalysisAnnotations.NotNullWhenFalse) != 0; bool maybeNullWhenTrue = (annotations & FlowAnalysisAnnotations.MaybeNullWhenTrue) != 0; bool maybeNullWhenFalse = (annotations & FlowAnalysisAnnotations.MaybeNullWhenFalse) != 0; if (maybeNullWhenFalse && !(maybeNullWhenTrue && notNullWhenFalse)) { // [MaybeNull, NotNullWhen(false)] means [MaybeNullWhen(true)] return TypeWithState.Create(typeWithState.Type, NullableFlowState.MaybeDefault); } else if (notNullWhenFalse) { return TypeWithState.Create(typeWithState.Type, NullableFlowState.NotNull); } return typeWithState; } void learnFromPostConditions(BoundExpression argument, TypeWithAnnotations parameterType, FlowAnalysisAnnotations parameterAnnotations) { // Note: NotNull = NotNullWhen(true) + NotNullWhen(false) bool notNullWhenTrue = (parameterAnnotations & FlowAnalysisAnnotations.NotNullWhenTrue) != 0; bool notNullWhenFalse = (parameterAnnotations & FlowAnalysisAnnotations.NotNullWhenFalse) != 0; bool disallowNull = (parameterAnnotations & FlowAnalysisAnnotations.DisallowNull) != 0; bool setNotNullFromParameterType = !argument.IsSuppressed && !parameterType.Type.IsPossiblyNullableReferenceTypeTypeParameter() && parameterType.NullableAnnotation.IsNotAnnotated(); // Note: MaybeNull = MaybeNullWhen(true) + MaybeNullWhen(false) bool maybeNullWhenTrue = (parameterAnnotations & FlowAnalysisAnnotations.MaybeNullWhenTrue) != 0; bool maybeNullWhenFalse = (parameterAnnotations & FlowAnalysisAnnotations.MaybeNullWhenFalse) != 0; if (maybeNullWhenTrue && maybeNullWhenFalse && !IsConditionalState && !(notNullWhenTrue && notNullWhenFalse)) { LearnFromNullTest(argument, ref State); } else if (((notNullWhenTrue && notNullWhenFalse) || disallowNull || setNotNullFromParameterType) && !IsConditionalState && !(maybeNullWhenTrue || maybeNullWhenFalse)) { LearnFromNonNullTest(argument, ref State); } else if (notNullWhenTrue || notNullWhenFalse || maybeNullWhenTrue || maybeNullWhenFalse) { Split(); if (notNullWhenTrue) { LearnFromNonNullTest(argument, ref StateWhenTrue); } if (notNullWhenFalse) { LearnFromNonNullTest(argument, ref StateWhenFalse); } if (maybeNullWhenTrue) { LearnFromNullTest(argument, ref StateWhenTrue); } if (maybeNullWhenFalse) { LearnFromNullTest(argument, ref StateWhenFalse); } } } } private (ImmutableArray arguments, ImmutableArray conversions) RemoveArgumentConversions( ImmutableArray arguments, ImmutableArray refKindsOpt) { int n = arguments.Length; var conversions = default(ImmutableArray); if (n > 0) { var argumentsBuilder = ArrayBuilder.GetInstance(n); var conversionsBuilder = ArrayBuilder.GetInstance(n); bool includedConversion = false; for (int i = 0; i < n; i++) { RefKind refKind = GetRefKind(refKindsOpt, i); var argument = arguments[i]; var conversion = Conversion.Identity; if (refKind == RefKind.None) { var before = argument; (argument, conversion) = RemoveConversion(argument, includeExplicitConversions: false); if (argument != before) { SnapshotWalkerThroughConversionGroup(before, argument); includedConversion = true; } } argumentsBuilder.Add(argument); conversionsBuilder.Add(conversion); } if (includedConversion) { arguments = argumentsBuilder.ToImmutable(); conversions = conversionsBuilder.ToImmutable(); } argumentsBuilder.Free(); conversionsBuilder.Free(); } return (arguments, conversions); } private VariableState GetVariableState(Optional localState) { return new VariableState( _variableSlot.ToImmutableDictionary(), ImmutableArray.Create(variableBySlot, start: 0, length: nextVariableSlot), _variableTypes.ToImmutableDictionary(_variableTypes.Comparer, TypeWithAnnotations.EqualsComparer.ConsiderEverythingComparer), localState.HasValue ? localState.Value : this.State.Clone()); } private (ParameterSymbol? Parameter, TypeWithAnnotations Type, FlowAnalysisAnnotations Annotations, bool isExpandedParamsArgument) GetCorrespondingParameter( int argumentOrdinal, ImmutableArray parameters, ImmutableArray argsToParamsOpt, bool expanded) { if (parameters.IsDefault) { return default; } int n = parameters.Length; ParameterSymbol? parameter; if (argsToParamsOpt.IsDefault) { if (argumentOrdinal < n) { parameter = parameters[argumentOrdinal]; } else if (expanded) { parameter = parameters[n - 1]; } else { parameter = null; } } else { int parameterOrdinal = argsToParamsOpt[argumentOrdinal]; if (parameterOrdinal < n) { parameter = parameters[parameterOrdinal]; } else { parameter = null; expanded = false; } } if (parameter is null) { Debug.Assert(!expanded); return default; } var type = parameter.TypeWithAnnotations; if (expanded && parameter.Ordinal == n - 1 && type.IsSZArray()) { type = ((ArrayTypeSymbol)type.Type).ElementTypeWithAnnotations; return (parameter, type, FlowAnalysisAnnotations.None, isExpandedParamsArgument: true); } return (parameter, type, GetParameterAnnotations(parameter), isExpandedParamsArgument: false); } private MethodSymbol InferMethodTypeArguments( Binder binder, MethodSymbol method, ImmutableArray arguments, ImmutableArray argumentRefKindsOpt, ImmutableArray argsToParamsOpt, bool expanded) { Debug.Assert(binder != null); Debug.Assert(method.IsGenericMethod); // https://github.com/dotnet/roslyn/issues/27961 OverloadResolution.IsMemberApplicableInNormalForm and // IsMemberApplicableInExpandedForm use the least overridden method. We need to do the same here. var definition = method.ConstructedFrom; var refKinds = ArrayBuilder.GetInstance(); if (argumentRefKindsOpt != null) { refKinds.AddRange(argumentRefKindsOpt); } // https://github.com/dotnet/roslyn/issues/27961 Do we really need OverloadResolution.GetEffectiveParameterTypes? // Aren't we doing roughly the same calculations in GetCorrespondingParameter? OverloadResolution.GetEffectiveParameterTypes( definition, arguments.Length, argsToParamsOpt, refKinds, isMethodGroupConversion: false, // https://github.com/dotnet/roslyn/issues/27961 `allowRefOmittedArguments` should be // false for constructors and several other cases (see Binder use). Should we // capture the original value in the BoundCall? allowRefOmittedArguments: true, binder: binder, expanded: expanded, parameterTypes: out ImmutableArray parameterTypes, parameterRefKinds: out ImmutableArray parameterRefKinds); refKinds.Free(); HashSet? useSiteDiagnostics = null; var result = MethodTypeInferrer.Infer( binder, _conversions, definition.TypeParameters, definition.ContainingType, parameterTypes, parameterRefKinds, arguments, ref useSiteDiagnostics, new MethodInferenceExtensions(this)); if (!result.Success) { return method; } return definition.Construct(result.InferredTypeArguments); } private sealed class MethodInferenceExtensions : MethodTypeInferrer.Extensions { private readonly NullableWalker _walker; internal MethodInferenceExtensions(NullableWalker walker) { _walker = walker; } internal override TypeWithAnnotations GetTypeWithAnnotations(BoundExpression expr) { return TypeWithAnnotations.Create(expr.Type, GetNullableAnnotation(expr)); } /// /// 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.DefaultLiteral: case BoundKind.DefaultExpression: case BoundKind.Literal: return (expr.ConstantValue?.IsNull != false) ? NullableAnnotation.NotAnnotated : NullableAnnotation.Annotated; case BoundKind.ExpressionWithNullability: return ((BoundExpressionWithNullability)expr).NullableAnnotation; case BoundKind.MethodGroup: case BoundKind.UnboundLambda: case BoundKind.UnconvertedObjectCreationExpression: return NullableAnnotation.NotAnnotated; default: Debug.Assert(false); // unexpected value return NullableAnnotation.Oblivious; } } internal override TypeWithAnnotations GetMethodGroupResultType(BoundMethodGroup group, MethodSymbol method) { if (_walker.TryGetMethodGroupReceiverNullability(group.ReceiverOpt, out TypeWithState receiverType)) { if (!method.IsStatic) { method = (MethodSymbol)AsMemberOfType(receiverType.Type, method); } } return method.ReturnTypeWithAnnotations; } } private ImmutableArray GetArgumentsForMethodTypeInference(ImmutableArray argumentResults, ImmutableArray arguments) { // https://github.com/dotnet/roslyn/issues/27961 MethodTypeInferrer.Infer relies // on the BoundExpressions for tuple element types and method groups. // By using a generic BoundValuePlaceholder, we're losing inference in those cases. // https://github.com/dotnet/roslyn/issues/27961 Inference should be based on // unconverted arguments. Consider cases such as `default`, lambdas, tuples. int n = argumentResults.Length; var builder = ArrayBuilder.GetInstance(n); for (int i = 0; i < n; i++) { var visitArgumentResult = argumentResults[i]; var lambdaState = visitArgumentResult.StateForLambda; // Note: for `out` arguments, the argument result contains the declaration type (see `VisitArgumentEvaluate`) var argumentResult = visitArgumentResult.RValueType.ToTypeWithAnnotations(compilation); builder.Add(getArgumentForMethodTypeInference(arguments[i], argumentResult, lambdaState)); } return builder.ToImmutableAndFree(); BoundExpression getArgumentForMethodTypeInference(BoundExpression argument, TypeWithAnnotations argumentType, Optional lambdaState) { if (argument.Kind == BoundKind.Lambda) { // MethodTypeInferrer must infer nullability for lambdas based on the nullability // from flow analysis rather than the declared nullability. To allow that, we need // to re-bind lambdas in MethodTypeInferrer. return getUnboundLambda((BoundLambda)argument, GetVariableState(lambdaState)); } if (!argumentType.HasType) { return argument; } if (argument is BoundLocal local && local.DeclarationKind == BoundLocalDeclarationKind.WithInferredType) { // 'out var' doesn't contribute to inference return new BoundExpressionWithNullability(argument.Syntax, argument, NullableAnnotation.Oblivious, type: null); } return new BoundExpressionWithNullability(argument.Syntax, argument, argumentType.NullableAnnotation, argumentType.Type); } static UnboundLambda getUnboundLambda(BoundLambda expr, VariableState variableState) { return expr.UnboundLambda.WithNullableState(expr.UnboundLambda.Data.Binder, variableState); } } private void CheckMethodConstraints(SyntaxNode syntax, MethodSymbol method) { if (_disableDiagnostics) { return; } var diagnosticsBuilder = ArrayBuilder.GetInstance(); var nullabilityBuilder = ArrayBuilder.GetInstance(); ArrayBuilder? useSiteDiagnosticsBuilder = null; ConstraintsHelper.CheckMethodConstraints( method, _conversions, compilation, diagnosticsBuilder, nullabilityBuilder, ref useSiteDiagnosticsBuilder); foreach (var pair in nullabilityBuilder) { Diagnostics.Add(pair.DiagnosticInfo, syntax.Location); } useSiteDiagnosticsBuilder?.Free(); nullabilityBuilder.Free(); diagnosticsBuilder.Free(); } /// /// Returns the expression without the top-most conversion plus the conversion. /// If the expression is not a conversion, returns the original expression plus /// the Identity conversion. If `includeExplicitConversions` is true, implicit and /// explicit conversions are considered. If `includeExplicitConversions` is false /// only implicit conversions are considered and if the expression is an explicit /// conversion, the expression is returned as is, with the Identity conversion. /// (Currently, the only visit method that passes `includeExplicitConversions: true` /// is VisitConversion. All other callers are handling implicit conversions only.) /// private static (BoundExpression expression, Conversion conversion) RemoveConversion(BoundExpression expr, bool includeExplicitConversions) { ConversionGroup? group = null; while (true) { if (expr.Kind != BoundKind.Conversion) { break; } var conversion = (BoundConversion)expr; if (group != conversion.ConversionGroupOpt && group != null) { // E.g.: (C)(B)a break; } group = conversion.ConversionGroupOpt; Debug.Assert(group != null || !conversion.ExplicitCastInCode); // Explicit conversions should include a group. if (!includeExplicitConversions && group?.IsExplicitConversion == true) { return (expr, Conversion.Identity); } expr = conversion.Operand; if (group == null) { // Ungrouped conversion should not be followed by another ungrouped // conversion. Otherwise, the conversions should have been grouped. // https://github.com/dotnet/roslyn/issues/34919 This assertion does not always hold true for // enum initializers //Debug.Assert(expr.Kind != BoundKind.Conversion || // ((BoundConversion)expr).ConversionGroupOpt != null || // ((BoundConversion)expr).ConversionKind == ConversionKind.NoConversion); return (expr, conversion.Conversion); } } return (expr, group?.Conversion ?? Conversion.Identity); } // See Binder.BindNullCoalescingOperator for initial binding. private Conversion GenerateConversionForConditionalOperator(BoundExpression sourceExpression, TypeSymbol? sourceType, TypeSymbol destinationType, bool reportMismatch) { var conversion = GenerateConversion(_conversions, sourceExpression, sourceType, destinationType, fromExplicitCast: false, extensionMethodThisArgument: false); bool canConvertNestedNullability = conversion.Exists; if (!canConvertNestedNullability && reportMismatch && !sourceExpression.IsSuppressed) { ReportNullabilityMismatchInAssignment(sourceExpression.Syntax, GetTypeAsDiagnosticArgument(sourceType), destinationType); } return conversion; } private static Conversion GenerateConversion(Conversions conversions, BoundExpression? sourceExpression, TypeSymbol? sourceType, TypeSymbol destinationType, bool fromExplicitCast, bool extensionMethodThisArgument) { HashSet? useSiteDiagnostics = null; bool useExpression = sourceType is null || UseExpressionForConversion(sourceExpression); if (extensionMethodThisArgument) { return conversions.ClassifyImplicitExtensionMethodThisArgConversion( useExpression ? sourceExpression : null, sourceType, destinationType, ref useSiteDiagnostics); } return useExpression ? (fromExplicitCast ? conversions.ClassifyConversionFromExpression(sourceExpression, destinationType, ref useSiteDiagnostics, forCast: true) : conversions.ClassifyImplicitConversionFromExpression(sourceExpression, destinationType, ref useSiteDiagnostics)) : (fromExplicitCast ? conversions.ClassifyConversionFromType(sourceType, destinationType, ref useSiteDiagnostics, forCast: true) : conversions.ClassifyImplicitConversionFromType(sourceType, destinationType, ref useSiteDiagnostics)); } /// /// Returns true if the expression should be used as the source when calculating /// a conversion from this expression, rather than using the type (with nullability) /// calculated by visiting this expression. Typically, that means expressions that /// do not have an explicit type but there are several other cases as well. /// (See expressions handled in ClassifyImplicitBuiltInConversionFromExpression.) /// private static bool UseExpressionForConversion([NotNullWhen(true)] BoundExpression? value) { if (value is null) { return false; } if (value.Type is null || value.Type.IsDynamic() || value.ConstantValue != null) { return true; } switch (value.Kind) { case BoundKind.InterpolatedString: return true; default: return false; } } /// /// Adjust declared type based on inferred nullability at the point of reference. /// private TypeWithState GetAdjustedResult(TypeWithState type, int slot) { if (slot > 0 && slot < this.State.Capacity) { NullableFlowState state = this.State[slot]; return TypeWithState.Create(type.Type, state); } return type; } /// /// Gets the corresponding member for a symbol from initial binding to match an updated receiver type in NullableWalker. /// For instance, this will map from List<string~>.Add(string~) to List<string?>.Add(string?) in the following example: /// /// string s = null; /// var list = new[] { s }.ToList(); /// list.Add(null); /// /// private static Symbol AsMemberOfType(TypeSymbol? type, Symbol symbol) { Debug.Assert((object)symbol != null); var containingType = type as NamedTypeSymbol; if (containingType is null || containingType.IsErrorType() || symbol is ErrorMethodSymbol) { return symbol; } if (symbol.Kind == SymbolKind.Method) { if (((MethodSymbol)symbol).MethodKind == MethodKind.LocalFunction) { // https://github.com/dotnet/roslyn/issues/27233 Handle type substitution for local functions. return symbol; } } if (symbol is TupleElementFieldSymbol) { return symbol.SymbolAsMember(containingType); } var symbolContainer = symbol.ContainingType; if (symbolContainer.IsAnonymousType) { int? memberIndex = symbol.Kind == SymbolKind.Property ? symbol.MemberIndexOpt : null; if (!memberIndex.HasValue) { return symbol; } return AnonymousTypeManager.GetAnonymousTypeProperty(containingType, memberIndex.GetValueOrDefault()); } if (!symbolContainer.IsGenericType) { Debug.Assert(symbol.ContainingType.IsDefinition); return symbol; } if (!containingType.IsGenericType) { return symbol; } if (symbolContainer.IsInterface) { if (tryAsMemberOfSingleType(containingType, out var result)) { return result; } foreach (var @interface in containingType.AllInterfacesNoUseSiteDiagnostics) { if (tryAsMemberOfSingleType(@interface, out result)) { return result; } } } else { while (true) { if (tryAsMemberOfSingleType(containingType, out var result)) { return result; } containingType = containingType.BaseTypeNoUseSiteDiagnostics; if ((object)containingType == null) { break; } } } Debug.Assert(false); // If this assert fails, add an appropriate test. return symbol; bool tryAsMemberOfSingleType(NamedTypeSymbol singleType, [NotNullWhen(true)] out Symbol? result) { if (!singleType.Equals(symbolContainer, TypeCompareKind.AllIgnoreOptions)) { result = null; return false; } var symbolDef = symbol.OriginalDefinition; result = symbolDef.SymbolAsMember(singleType); if (result is MethodSymbol resultMethod && resultMethod.IsGenericMethod) { result = resultMethod.Construct(((MethodSymbol)symbol).TypeArgumentsWithAnnotations); } return true; } } public override BoundNode? VisitConversion(BoundConversion node) { // https://github.com/dotnet/roslyn/issues/35732: Assert VisitConversion is only used for explicit conversions. //Debug.Assert(node.ExplicitCastInCode); //Debug.Assert(node.ConversionGroupOpt != null); //Debug.Assert(node.ConversionGroupOpt.ExplicitType.HasType); TypeWithAnnotations explicitType = node.ConversionGroupOpt?.ExplicitType ?? default; bool fromExplicitCast = explicitType.HasType; TypeWithAnnotations targetType = fromExplicitCast ? explicitType : TypeWithAnnotations.Create(node.Type); Debug.Assert(targetType.HasType); (BoundExpression operand, Conversion conversion) = RemoveConversion(node, includeExplicitConversions: true); SnapshotWalkerThroughConversionGroup(node, operand); TypeWithState operandType = VisitRvalueWithState(operand); SetResultType(node, VisitConversion( node, operand, conversion, targetType, operandType, checkConversion: true, fromExplicitCast: fromExplicitCast, useLegacyWarnings: fromExplicitCast, AssignmentKind.Assignment, reportTopLevelWarnings: fromExplicitCast, reportRemainingWarnings: true, trackMembers: true)); return null; } /// /// Visit an expression. If an explicit target type is provided, the expression is converted /// to that type. This method should be called whenever an expression may contain /// an implicit conversion, even if that conversion was omitted from the bound tree, /// so the conversion can be re-classified with nullability. /// private TypeWithState VisitOptionalImplicitConversion(BoundExpression expr, TypeWithAnnotations targetTypeOpt, bool useLegacyWarnings, bool trackMembers, AssignmentKind assignmentKind) { if (!targetTypeOpt.HasType) { return VisitRvalueWithState(expr); } (BoundExpression operand, Conversion conversion) = RemoveConversion(expr, includeExplicitConversions: false); SnapshotWalkerThroughConversionGroup(expr, operand); var operandType = VisitRvalueWithState(operand); // If an explicit conversion was used in place of an implicit conversion, the explicit // conversion was created by initial binding after reporting "error CS0266: // Cannot implicitly convert type '...' to '...'. An explicit conversion exists ...". // Since an error was reported, we don't need to report nested warnings as well. bool reportNestedWarnings = !conversion.IsExplicit; var resultType = VisitConversion( GetConversionIfApplicable(expr, operand), operand, conversion, targetTypeOpt, operandType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: useLegacyWarnings, assignmentKind, reportTopLevelWarnings: true, reportRemainingWarnings: reportNestedWarnings, trackMembers: trackMembers); return resultType; } private static bool AreNullableAndUnderlyingTypes([NotNullWhen(true)] TypeSymbol? nullableTypeOpt, [NotNullWhen(true)] TypeSymbol? underlyingTypeOpt, out TypeWithAnnotations underlyingTypeWithAnnotations) { if (nullableTypeOpt?.IsNullableType() == true && underlyingTypeOpt?.IsNullableType() == false) { var typeArg = nullableTypeOpt.GetNullableUnderlyingTypeWithAnnotations(); if (typeArg.Type.Equals(underlyingTypeOpt, TypeCompareKind.AllIgnoreOptions)) { underlyingTypeWithAnnotations = typeArg; return true; } } underlyingTypeWithAnnotations = default; return false; } public override BoundNode? VisitTupleLiteral(BoundTupleLiteral node) { VisitTupleExpression(node); return null; } public override BoundNode? VisitConvertedTupleLiteral(BoundConvertedTupleLiteral node) { Debug.Assert(!IsConditionalState); var savedState = this.State.Clone(); // Visit the source tuple so that the semantic model can correctly report nullability for it // Disable diagnostics, as we don't want to duplicate any that are produced by visiting the converted literal below VisitWithoutDiagnostics(node.SourceTuple); this.SetState(savedState); VisitTupleExpression(node); return null; } private void VisitTupleExpression(BoundTupleExpression node) { var arguments = node.Arguments; ImmutableArray elementTypes = arguments.SelectAsArray((a, w) => w.VisitRvalueWithState(a), this); ImmutableArray elementTypesWithAnnotations = elementTypes.SelectAsArray(a => a.ToTypeWithAnnotations(compilation)); var tupleOpt = (NamedTypeSymbol?)node.Type; if (tupleOpt is null) { SetResultType(node, TypeWithState.Create(null, NullableFlowState.NotNull)); } else { int slot = GetOrCreatePlaceholderSlot(node); if (slot > 0) { this.State[slot] = NullableFlowState.NotNull; TrackNullableStateOfTupleElements(slot, tupleOpt, arguments, elementTypes, argsToParamsOpt: default, useRestField: false); } tupleOpt = tupleOpt.WithElementTypes(elementTypesWithAnnotations); if (!_disableDiagnostics) { var locations = tupleOpt.TupleElements.SelectAsArray((element, location) => element.Locations.FirstOrDefault() ?? location, node.Syntax.Location); tupleOpt.CheckConstraints(_conversions, typeSyntax: node.Syntax, locations, currentCompilation: compilation, diagnosticsOpt: null, nullabilityDiagnosticsOpt: Diagnostics); } SetResultType(node, TypeWithState.Create(tupleOpt, NullableFlowState.NotNull)); } } /// /// Set the nullability of tuple elements for tuples at the point of construction. /// If is true, the tuple was constructed with an explicit /// 'new ValueTuple' call, in which case the 8-th element, if any, represents the 'Rest' field. /// private void TrackNullableStateOfTupleElements( int slot, NamedTypeSymbol tupleType, ImmutableArray values, ImmutableArray types, ImmutableArray argsToParamsOpt, bool useRestField) { Debug.Assert(tupleType.IsTupleType); Debug.Assert(values.Length == types.Length); Debug.Assert(values.Length == (useRestField ? Math.Min(tupleType.TupleElements.Length, NamedTypeSymbol.ValueTupleRestPosition) : tupleType.TupleElements.Length)); if (slot > 0) { var tupleElements = tupleType.TupleElements; int n = values.Length; if (useRestField) { n = Math.Min(n, NamedTypeSymbol.ValueTupleRestPosition - 1); } for (int i = 0; i < n; i++) { var argOrdinal = GetArgumentOrdinalFromParameterOrdinal(i); trackState(values[argOrdinal], tupleElements[i], types[argOrdinal]); } if (useRestField && values.Length == NamedTypeSymbol.ValueTupleRestPosition && tupleType.GetMembers(NamedTypeSymbol.ValueTupleRestFieldName).FirstOrDefault() is FieldSymbol restField) { var argOrdinal = GetArgumentOrdinalFromParameterOrdinal(NamedTypeSymbol.ValueTupleRestPosition - 1); trackState(values[argOrdinal], restField, types[argOrdinal]); } } void trackState(BoundExpression value, FieldSymbol field, TypeWithState valueType) { int targetSlot = GetOrCreateSlot(field, slot); TrackNullableStateForAssignment(value, field.TypeWithAnnotations, targetSlot, valueType, MakeSlot(value)); } int GetArgumentOrdinalFromParameterOrdinal(int parameterOrdinal) { var index = argsToParamsOpt.IsDefault ? parameterOrdinal : argsToParamsOpt.IndexOf(parameterOrdinal); Debug.Assert(index != -1); return index; } } private void TrackNullableStateOfNullableValue(int containingSlot, TypeSymbol containingType, BoundExpression? value, TypeWithState valueType, int valueSlot) { Debug.Assert(containingType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T); Debug.Assert(containingSlot > 0); Debug.Assert(valueSlot > 0); int targetSlot = GetNullableOfTValueSlot(containingType, containingSlot, out Symbol? symbol); if (targetSlot > 0) { TrackNullableStateForAssignment(value, symbol!.GetTypeOrReturnType(), targetSlot, valueType, valueSlot); } } private void TrackNullableStateOfNullableValue(BoundExpression node, BoundExpression operand, TypeSymbol convertedType, TypeWithAnnotations underlyingType) { int valueSlot = MakeSlot(operand); if (valueSlot > 0) { int containingSlot = GetOrCreatePlaceholderSlot(node); Debug.Assert(containingSlot > 0); TrackNullableStateOfNullableValue(containingSlot, convertedType, operand, underlyingType.ToTypeWithState(), valueSlot); } } private void TrackNullableStateOfTupleConversion( BoundConversion? conversionOpt, BoundExpression convertedNode, Conversion conversion, TypeSymbol targetType, TypeSymbol operandType, int slot, int valueSlot, AssignmentKind assignmentKind, ParameterSymbol? parameterOpt, bool reportWarnings) { Debug.Assert(conversion.Kind == ConversionKind.ImplicitTuple || conversion.Kind == ConversionKind.ExplicitTuple); Debug.Assert(slot > 0); Debug.Assert(valueSlot > 0); var valueTuple = operandType as NamedTypeSymbol; if (valueTuple is null || !valueTuple.IsTupleType) { return; } var conversions = conversion.UnderlyingConversions; var targetElements = ((NamedTypeSymbol)targetType).TupleElements; var valueElements = valueTuple.TupleElements; int n = valueElements.Length; for (int i = 0; i < n; i++) { trackConvertedValue(targetElements[i], conversions[i], valueElements[i]); } void trackConvertedValue(FieldSymbol targetField, Conversion conversion, FieldSymbol valueField) { switch (conversion.Kind) { case ConversionKind.Identity: case ConversionKind.NullLiteral: case ConversionKind.DefaultLiteral: case ConversionKind.ImplicitReference: case ConversionKind.ExplicitReference: case ConversionKind.Boxing: case ConversionKind.Unboxing: InheritNullableStateOfMember(slot, valueSlot, valueField, isDefaultValue: false, skipSlot: slot); break; case ConversionKind.ImplicitTupleLiteral: case ConversionKind.ExplicitTupleLiteral: case ConversionKind.ImplicitTuple: case ConversionKind.ExplicitTuple: { int targetFieldSlot = GetOrCreateSlot(targetField, slot); if (targetFieldSlot > 0) { this.State[targetFieldSlot] = NullableFlowState.NotNull; int valueFieldSlot = GetOrCreateSlot(valueField, valueSlot); if (valueFieldSlot > 0) { TrackNullableStateOfTupleConversion(conversionOpt, convertedNode, conversion, targetField.Type, valueField.Type, targetFieldSlot, valueFieldSlot, assignmentKind, parameterOpt, reportWarnings); } } } break; case ConversionKind.ImplicitNullable: case ConversionKind.ExplicitNullable: // Conversion of T to Nullable is equivalent to new Nullable(t). if (AreNullableAndUnderlyingTypes(targetField.Type, valueField.Type, out _)) { int targetFieldSlot = GetOrCreateSlot(targetField, slot); if (targetFieldSlot > 0) { this.State[targetFieldSlot] = NullableFlowState.NotNull; int valueFieldSlot = GetOrCreateSlot(valueField, valueSlot); if (valueFieldSlot > 0) { TrackNullableStateOfNullableValue(targetFieldSlot, targetField.Type, null, valueField.TypeWithAnnotations.ToTypeWithState(), valueFieldSlot); } } } break; case ConversionKind.ImplicitUserDefined: case ConversionKind.ExplicitUserDefined: { var convertedType = VisitUserDefinedConversion( conversionOpt, convertedNode, conversion, targetField.TypeWithAnnotations, valueField.TypeWithAnnotations.ToTypeWithState(), useLegacyWarnings: false, assignmentKind, parameterOpt, reportTopLevelWarnings: reportWarnings, reportRemainingWarnings: reportWarnings, diagnosticLocation: (conversionOpt ?? convertedNode).Syntax.GetLocation()); int targetFieldSlot = GetOrCreateSlot(targetField, slot); if (targetFieldSlot > 0) { this.State[targetFieldSlot] = convertedType.State; } } break; default: break; } } } public override BoundNode? VisitTupleBinaryOperator(BoundTupleBinaryOperator node) { base.VisitTupleBinaryOperator(node); SetNotNullResult(node); return null; } private void ReportNullabilityMismatchWithTargetDelegate(Location location, TypeSymbol targetType, MethodSymbol targetInvokeMethod, MethodSymbol sourceInvokeMethod, bool invokedAsExtensionMethod) { SourceMemberContainerTypeSymbol.CheckValidNullableMethodOverride( compilation, targetInvokeMethod, sourceInvokeMethod, Diagnostics, reportBadDelegateReturn, reportBadDelegateParameter, extraArgument: (targetType, location), invokedAsExtensionMethod: invokedAsExtensionMethod); void reportBadDelegateReturn(DiagnosticBag bag, MethodSymbol targetInvokeMethod, MethodSymbol sourceInvokeMethod, bool topLevel, (TypeSymbol targetType, Location location) arg) { ReportDiagnostic(ErrorCode.WRN_NullabilityMismatchInReturnTypeOfTargetDelegate, arg.location, new FormattedSymbol(sourceInvokeMethod, SymbolDisplayFormat.MinimallyQualifiedFormat), arg.targetType); } void reportBadDelegateParameter(DiagnosticBag bag, MethodSymbol sourceInvokeMethod, MethodSymbol targetInvokeMethod, ParameterSymbol parameter, bool topLevel, (TypeSymbol targetType, Location location) arg) { ReportDiagnostic(ErrorCode.WRN_NullabilityMismatchInParameterTypeOfTargetDelegate, arg.location, GetParameterAsDiagnosticArgument(parameter), GetContainingSymbolAsDiagnosticArgument(parameter), arg.targetType); } } private void ReportNullabilityMismatchWithTargetDelegate(Location location, NamedTypeSymbol delegateType, UnboundLambda unboundLambda) { if (!unboundLambda.HasExplicitlyTypedParameterList) { return; } var invoke = delegateType?.DelegateInvokeMethod; if (invoke is null) { return; } Debug.Assert(delegateType is object); int count = Math.Min(invoke.ParameterCount, unboundLambda.ParameterCount); for (int i = 0; i < count; i++) { var invokeParameter = invoke.Parameters[i]; // Parameter nullability is expected to match exactly. This corresponds to the behavior of initial binding. // Action x = (object o) => { }; // error CS1661: Cannot convert lambda expression to delegate type 'Action' because the parameter types do not match the delegate parameter types // Action y = (object? o) => { }; // warning CS8622: Nullability of reference types in type of parameter 'o' of 'lambda expression' doesn't match the target delegate 'Action'. // https://github.com/dotnet/roslyn/issues/35564: Consider relaxing and allow implicit conversions of nullability. // (Compare with method group conversions which pass `requireIdentity: false`.) if (IsNullabilityMismatch(invokeParameter.TypeWithAnnotations, unboundLambda.ParameterTypeWithAnnotations(i), requireIdentity: true)) { // Should the warning be reported using location of specific lambda parameter? ReportDiagnostic(ErrorCode.WRN_NullabilityMismatchInParameterTypeOfTargetDelegate, location, unboundLambda.ParameterName(i), unboundLambda.MessageID.Localize(), delegateType); } } } private bool IsNullabilityMismatch(TypeWithAnnotations source, TypeWithAnnotations destination, bool requireIdentity) { if (!HasTopLevelNullabilityConversion(source, destination, requireIdentity)) { return true; } if (requireIdentity) { return IsNullabilityMismatch(source, destination); } var sourceType = source.Type; var destinationType = destination.Type; HashSet? useSiteDiagnostics = null; return !_conversions.ClassifyImplicitConversionFromType(sourceType, destinationType, ref useSiteDiagnostics).Exists; } private bool HasTopLevelNullabilityConversion(TypeWithAnnotations source, TypeWithAnnotations destination, bool requireIdentity) { return requireIdentity ? _conversions.HasTopLevelNullabilityIdentityConversion(source, destination) : _conversions.HasTopLevelNullabilityImplicitConversion(source, destination); } /// /// Gets the conversion node for passing to , /// if one should be passed. /// private static BoundConversion? GetConversionIfApplicable(BoundExpression? conversionOpt, BoundExpression convertedNode) { Debug.Assert(conversionOpt is null || convertedNode == conversionOpt // Note that convertedNode itself can be a BoundConversion, so we do this check explicitly // because the below calls to RemoveConversion could potentially strip that conversion. || convertedNode == RemoveConversion(conversionOpt, includeExplicitConversions: false).expression || convertedNode == RemoveConversion(conversionOpt, includeExplicitConversions: true).expression); return conversionOpt == convertedNode ? null : (BoundConversion?)conversionOpt; } /// /// Apply the conversion to the type of the operand and return the resulting type. (If the /// operand does not have an explicit type, the operand expression is used for the type.) /// If `checkConversion` is set, the incoming conversion is assumed to be from binding and will be /// re-calculated, this time considering nullability. (Note that the conversion calculation considers /// nested nullability only. The caller is responsible for checking the top-level nullability of /// the type returned by this method.) `trackMembers` should be set if the nullability of any /// members of the operand should be copied to the converted result when possible. /// private TypeWithState VisitConversion( BoundConversion? conversionOpt, BoundExpression conversionOperand, Conversion conversion, TypeWithAnnotations targetTypeWithNullability, TypeWithState operandType, bool checkConversion, bool fromExplicitCast, bool useLegacyWarnings, AssignmentKind assignmentKind, ParameterSymbol? parameterOpt = null, bool reportTopLevelWarnings = true, bool reportRemainingWarnings = true, bool extensionMethodThisArgument = false, Optional stateForLambda = default, bool trackMembers = false, Location? diagnosticLocationOpt = null) { Debug.Assert(!trackMembers || !IsConditionalState); Debug.Assert(conversionOperand != null); NullableFlowState resultState = NullableFlowState.NotNull; bool canConvertNestedNullability = true; bool isSuppressed = false; diagnosticLocationOpt ??= (conversionOpt ?? conversionOperand).Syntax.GetLocation(); if (conversionOperand.IsSuppressed == true) { reportTopLevelWarnings = false; reportRemainingWarnings = false; isSuppressed = true; } #nullable disable TypeSymbol targetType = targetTypeWithNullability.Type; switch (conversion.Kind) { case ConversionKind.MethodGroup: { var group = conversionOperand as BoundMethodGroup; var (invokeSignature, parameters) = getDelegateOrFunctionPointerInfo(targetType); var method = conversion.Method; if (group != null) { if (method?.OriginalDefinition is LocalFunctionSymbol localFunc) { VisitLocalFunctionUse(localFunc); } method = CheckMethodGroupReceiverNullability(group, parameters, method, conversion.IsExtensionMethod); } if (reportRemainingWarnings) { ReportNullabilityMismatchWithTargetDelegate(diagnosticLocationOpt, targetType, invokeSignature, method, conversion.IsExtensionMethod); } } resultState = NullableFlowState.NotNull; break; static (MethodSymbol invokeSignature, ImmutableArray) getDelegateOrFunctionPointerInfo(TypeSymbol targetType) => targetType switch { NamedTypeSymbol { TypeKind: TypeKind.Delegate, DelegateInvokeMethod: { Parameters: { } parameters } signature } => (signature, parameters), FunctionPointerTypeSymbol { Signature: { Parameters: { } parameters } signature } => (signature, parameters), _ => throw ExceptionUtilities.UnexpectedValue(targetType) }; case ConversionKind.AnonymousFunction: if (conversionOperand is BoundLambda lambda) { var delegateType = targetType.GetDelegateType(); VisitLambda(lambda, delegateType, stateForLambda); if (reportRemainingWarnings) { ReportNullabilityMismatchWithTargetDelegate(diagnosticLocationOpt, delegateType, lambda.UnboundLambda); } TrackAnalyzedNullabilityThroughConversionGroup(targetTypeWithNullability.ToTypeWithState(), conversionOpt, conversionOperand); return TypeWithState.Create(targetType, NullableFlowState.NotNull); } break; case ConversionKind.InterpolatedString: resultState = NullableFlowState.NotNull; break; case ConversionKind.ObjectCreation: case ConversionKind.SwitchExpression: case ConversionKind.ConditionalExpression: return operandType; case ConversionKind.ExplicitUserDefined: case ConversionKind.ImplicitUserDefined: return VisitUserDefinedConversion(conversionOpt, conversionOperand, conversion, targetTypeWithNullability, operandType, useLegacyWarnings, assignmentKind, parameterOpt, reportTopLevelWarnings, reportRemainingWarnings, diagnosticLocationOpt); case ConversionKind.ExplicitDynamic: case ConversionKind.ImplicitDynamic: resultState = getConversionResultState(operandType); break; case ConversionKind.Boxing: resultState = getBoxingConversionResultState(targetTypeWithNullability, operandType); break; case ConversionKind.Unboxing: if (targetType.IsNonNullableValueType()) { if (!operandType.IsNotNull && reportRemainingWarnings) { ReportDiagnostic(ErrorCode.WRN_UnboxPossibleNull, diagnosticLocationOpt); } LearnFromNonNullTest(conversionOperand, ref State); } else { resultState = getUnboxingConversionResultState(operandType); } break; case ConversionKind.ImplicitThrow: resultState = NullableFlowState.NotNull; break; case ConversionKind.NoConversion: resultState = getConversionResultState(operandType); break; case ConversionKind.NullLiteral: case ConversionKind.DefaultLiteral: checkConversion = false; goto case ConversionKind.Identity; case ConversionKind.Identity: // If the operand is an explicit conversion, and this identity conversion // is converting to the same type including nullability, skip the conversion // to avoid reporting redundant warnings. Also check useLegacyWarnings // since that value was used when reporting warnings for the explicit cast. // Don't skip the node when it's a user-defined conversion, as identity conversions // on top of user-defined conversions means that we're coming in from VisitUserDefinedConversion // and that any warnings caught by this recursive call of VisitConversion won't be redundant. if (useLegacyWarnings && conversionOperand is BoundConversion operandConversion && !operandConversion.ConversionKind.IsUserDefinedConversion()) { var explicitType = operandConversion.ConversionGroupOpt?.ExplicitType; if (explicitType?.Equals(targetTypeWithNullability, TypeCompareKind.ConsiderEverything) == true) { TrackAnalyzedNullabilityThroughConversionGroup( calculateResultType(targetTypeWithNullability, fromExplicitCast, operandType.State, isSuppressed, targetType), conversionOpt, conversionOperand); return operandType; } } if (operandType.Type?.IsTupleType == true || conversionOperand.Kind == BoundKind.TupleLiteral) { goto case ConversionKind.ImplicitTuple; } goto case ConversionKind.ImplicitReference; case ConversionKind.ImplicitReference: case ConversionKind.ExplicitReference: // Inherit state from the operand. if (checkConversion) { conversion = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument); canConvertNestedNullability = conversion.Exists; } resultState = conversion.IsReference ? getReferenceConversionResultState(targetTypeWithNullability, operandType) : operandType.State; break; case ConversionKind.ImplicitNullable: if (trackMembers) { Debug.Assert(conversionOperand != null); if (AreNullableAndUnderlyingTypes(targetType, operandType.Type, out TypeWithAnnotations underlyingType)) { // Conversion of T to Nullable is equivalent to new Nullable(t). int valueSlot = MakeSlot(conversionOperand); if (valueSlot > 0) { int containingSlot = GetOrCreatePlaceholderSlot(conversionOpt); Debug.Assert(containingSlot > 0); TrackNullableStateOfNullableValue(containingSlot, targetType, conversionOperand, underlyingType.ToTypeWithState(), valueSlot); } } } if (checkConversion) { conversion = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument); canConvertNestedNullability = conversion.Exists; } resultState = operandType.State; break; case ConversionKind.ExplicitNullable: if (operandType.Type?.IsNullableType() == true && !targetType.IsNullableType()) { // Explicit conversion of Nullable to T is equivalent to Nullable.Value. if (reportTopLevelWarnings && operandType.MayBeNull) { ReportDiagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, diagnosticLocationOpt); } // Mark the value as not nullable, regardless of whether it was known to be nullable, // because the implied call to `.Value` will only succeed if not null. if (conversionOperand != null) { LearnFromNonNullTest(conversionOperand, ref State); } } goto case ConversionKind.ImplicitNullable; case ConversionKind.ImplicitTuple: case ConversionKind.ImplicitTupleLiteral: case ConversionKind.ExplicitTupleLiteral: case ConversionKind.ExplicitTuple: if (trackMembers) { Debug.Assert(conversionOperand != null); switch (conversion.Kind) { case ConversionKind.ImplicitTuple: case ConversionKind.ExplicitTuple: int valueSlot = MakeSlot(conversionOperand); if (valueSlot > 0) { int slot = GetOrCreatePlaceholderSlot(conversionOpt); if (slot > 0) { TrackNullableStateOfTupleConversion(conversionOpt, conversionOperand, conversion, targetType, operandType.Type, slot, valueSlot, assignmentKind, parameterOpt, reportWarnings: reportRemainingWarnings); } } break; } } if (checkConversion && !targetType.IsErrorType()) { // https://github.com/dotnet/roslyn/issues/29699: Report warnings for user-defined conversions on tuple elements. conversion = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument); canConvertNestedNullability = conversion.Exists; } resultState = NullableFlowState.NotNull; break; case ConversionKind.Deconstruction: // Can reach here, with an error type, when the // Deconstruct method is missing or inaccessible. break; case ConversionKind.ExplicitEnumeration: // Can reach here, with an error type. break; default: Debug.Assert(targetType.IsValueType || targetType.IsErrorType()); break; } TypeWithState resultType = calculateResultType(targetTypeWithNullability, fromExplicitCast, resultState, isSuppressed, targetType); if (operandType.Type?.IsErrorType() != true && !targetType.IsErrorType()) { // Need to report all warnings that apply since the warnings can be suppressed individually. if (reportTopLevelWarnings) { ReportNullableAssignmentIfNecessary(conversionOperand, targetTypeWithNullability, resultType, useLegacyWarnings, assignmentKind, parameterOpt, diagnosticLocationOpt); } if (reportRemainingWarnings && !canConvertNestedNullability) { if (assignmentKind == AssignmentKind.Argument) { ReportNullabilityMismatchInArgument(diagnosticLocationOpt, operandType.Type, parameterOpt, targetType, forOutput: false); } else { ReportNullabilityMismatchInAssignment(diagnosticLocationOpt, GetTypeAsDiagnosticArgument(operandType.Type), targetType); } } } TrackAnalyzedNullabilityThroughConversionGroup(resultType, conversionOpt, conversionOperand); return resultType; #nullable enable static TypeWithState calculateResultType(TypeWithAnnotations targetTypeWithNullability, bool fromExplicitCast, NullableFlowState resultState, bool isSuppressed, TypeSymbol targetType) { if (isSuppressed) { resultState = NullableFlowState.NotNull; } else if (fromExplicitCast && targetTypeWithNullability.NullableAnnotation.IsAnnotated() && !targetType.IsNullableType()) { // An explicit cast to a nullable reference type introduces nullability resultState = targetType?.IsTypeParameterDisallowingAnnotationInCSharp8() == true ? NullableFlowState.MaybeDefault : NullableFlowState.MaybeNull; } var resultType = TypeWithState.Create(targetType, resultState); return resultType; } static NullableFlowState getReferenceConversionResultState(TypeWithAnnotations targetType, TypeWithState operandType) { var state = operandType.State; switch (state) { case NullableFlowState.MaybeNull: if (targetType.Type?.IsTypeParameterDisallowingAnnotationInCSharp8() == true) { var type = operandType.Type; if (type is null || !type.IsTypeParameterDisallowingAnnotationInCSharp8()) { return NullableFlowState.MaybeDefault; } else if (targetType.NullableAnnotation.IsNotAnnotated() && type is TypeParameterSymbol typeParameter1 && dependsOnTypeParameter(typeParameter1, (TypeParameterSymbol)targetType.Type, NullableAnnotation.NotAnnotated, out var annotation)) { return (annotation == NullableAnnotation.Annotated) ? NullableFlowState.MaybeDefault : NullableFlowState.MaybeNull; } } break; case NullableFlowState.MaybeDefault: if (targetType.Type?.IsTypeParameterDisallowingAnnotationInCSharp8() == false) { return NullableFlowState.MaybeNull; } break; } return state; } // Converting to a less-derived type (object, interface, type parameter). // If the operand is MaybeNull, the result should be // MaybeNull (if the target type allows) or MaybeDefault otherwise. static NullableFlowState getBoxingConversionResultState(TypeWithAnnotations targetType, TypeWithState operandType) { var state = operandType.State; if (state == NullableFlowState.MaybeNull) { var type = operandType.Type; if (type is null || !type.IsTypeParameterDisallowingAnnotationInCSharp8()) { return NullableFlowState.MaybeDefault; } else if (targetType.NullableAnnotation.IsNotAnnotated() && type is TypeParameterSymbol typeParameter1 && targetType.Type is TypeParameterSymbol typeParameter2) { bool dependsOn = dependsOnTypeParameter(typeParameter1, typeParameter2, NullableAnnotation.NotAnnotated, out var annotation); Debug.Assert(dependsOn); // If this case fails, add a corresponding test. if (dependsOn) { return (annotation == NullableAnnotation.Annotated) ? NullableFlowState.MaybeDefault : NullableFlowState.MaybeNull; } } } return state; } // Converting to a more-derived type (struct, class, type parameter). // If the operand is MaybeNull or MaybeDefault, the result should be // MaybeDefault. static NullableFlowState getUnboxingConversionResultState(TypeWithState operandType) { var state = operandType.State; if (state == NullableFlowState.MaybeNull) { return NullableFlowState.MaybeDefault; } return state; } static NullableFlowState getConversionResultState(TypeWithState operandType) { var state = operandType.State; if (state == NullableFlowState.MaybeNull) { return NullableFlowState.MaybeDefault; } return state; } // If type parameter 1 depends on type parameter 2 (that is, if type parameter 2 appears // in the constraint types of type parameter 1), returns the effective annotation on // type parameter 2 in the constraints of type parameter 1. static bool dependsOnTypeParameter(TypeParameterSymbol typeParameter1, TypeParameterSymbol typeParameter2, NullableAnnotation typeParameter1Annotation, out NullableAnnotation annotation) { if (typeParameter1.Equals(typeParameter2, TypeCompareKind.AllIgnoreOptions)) { annotation = typeParameter1Annotation; return true; } bool dependsOn = false; var combinedAnnotation = NullableAnnotation.Annotated; foreach (var constraintType in typeParameter1.ConstraintTypesNoUseSiteDiagnostics) { if (constraintType.Type is TypeParameterSymbol constraintTypeParameter && dependsOnTypeParameter(constraintTypeParameter, typeParameter2, constraintType.NullableAnnotation, out var constraintAnnotation)) { dependsOn = true; combinedAnnotation = combinedAnnotation.Meet(constraintAnnotation); } } if (dependsOn) { annotation = combinedAnnotation.Join(typeParameter1Annotation); return true; } annotation = default; return false; } } private TypeWithState VisitUserDefinedConversion( BoundConversion? conversionOpt, BoundExpression conversionOperand, Conversion conversion, TypeWithAnnotations targetTypeWithNullability, TypeWithState operandType, bool useLegacyWarnings, AssignmentKind assignmentKind, ParameterSymbol? parameterOpt, bool reportTopLevelWarnings, bool reportRemainingWarnings, Location diagnosticLocation) { Debug.Assert(!IsConditionalState); Debug.Assert(conversionOperand != null); Debug.Assert(targetTypeWithNullability.HasType); Debug.Assert(diagnosticLocation != null); Debug.Assert(conversion.Kind == ConversionKind.ExplicitUserDefined || conversion.Kind == ConversionKind.ImplicitUserDefined); TypeSymbol targetType = targetTypeWithNullability.Type; // cf. Binder.CreateUserDefinedConversion if (!conversion.IsValid) { var resultType = TypeWithState.Create(targetType, NullableFlowState.NotNull); TrackAnalyzedNullabilityThroughConversionGroup(resultType, conversionOpt, conversionOperand); return resultType; } // operand -> conversion "from" type // May be distinct from method parameter type for Nullable. operandType = VisitConversion( conversionOpt, conversionOperand, conversion.UserDefinedFromConversion, TypeWithAnnotations.Create(conversion.BestUserDefinedConversionAnalysis!.FromType), operandType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings, assignmentKind, parameterOpt, reportTopLevelWarnings, reportRemainingWarnings, diagnosticLocationOpt: diagnosticLocation); // Update method based on operandType: see https://github.com/dotnet/roslyn/issues/29605. // (see NullableReferenceTypesTests.ImplicitConversions_07). var method = conversion.Method; Debug.Assert(method is object); Debug.Assert(method.ParameterCount == 1); Debug.Assert(operandType.Type is object); var parameter = method.Parameters[0]; var parameterAnnotations = GetParameterAnnotations(parameter); var parameterType = ApplyLValueAnnotations(parameter.TypeWithAnnotations, parameterAnnotations); TypeWithState underlyingOperandType = default; bool isLiftedConversion = false; if (operandType.Type.IsNullableType() && !parameterType.IsNullableType()) { var underlyingOperandTypeWithAnnotations = operandType.Type.GetNullableUnderlyingTypeWithAnnotations(); underlyingOperandType = underlyingOperandTypeWithAnnotations.ToTypeWithState(); isLiftedConversion = parameterType.Equals(underlyingOperandTypeWithAnnotations, TypeCompareKind.AllIgnoreOptions); } // conversion "from" type -> method parameter type NullableFlowState operandState = operandType.State; Location operandLocation = conversionOperand.Syntax.GetLocation(); _ = ClassifyAndVisitConversion( conversionOperand, parameterType, isLiftedConversion ? underlyingOperandType : operandType, useLegacyWarnings, AssignmentKind.Argument, parameterOpt: parameter, reportWarnings: reportRemainingWarnings, fromExplicitCast: false, diagnosticLocation: operandLocation); // in the case of a lifted conversion, we assume that the call to the operator occurs only if the argument is not-null if (!isLiftedConversion) { CheckDisallowedNullAssignment(operandType, parameterAnnotations, conversionOperand.Syntax.Location); } // method parameter type -> method return type var methodReturnType = method.ReturnTypeWithAnnotations; operandType = GetLiftedReturnTypeIfNecessary(isLiftedConversion, methodReturnType, operandState); if (!isLiftedConversion || operandState.IsNotNull()) { var returnNotNull = operandState.IsNotNull() && method.ReturnNotNullIfParameterNotNull.Contains(parameter.Name); if (returnNotNull) { operandType = operandType.WithNotNullState(); } else { operandType = ApplyUnconditionalAnnotations(operandType, GetRValueAnnotations(method)); } } // method return type -> conversion "to" type // May be distinct from method return type for Nullable. operandType = ClassifyAndVisitConversion( conversionOperand, TypeWithAnnotations.Create(conversion.BestUserDefinedConversionAnalysis!.ToType), operandType, useLegacyWarnings, assignmentKind, parameterOpt, reportWarnings: reportRemainingWarnings, fromExplicitCast: false, diagnosticLocation: operandLocation); // conversion "to" type -> final type // We should only pass fromExplicitCast here. Given the following example: // // class A { public static explicit operator C(A a) => throw null!; } // class C // { // void M() => var c = (C?)new A(); // } // // This final conversion from the method return type "C" to the cast type "C?" is // where we will need to ensure that this is counted as an explicit cast, so that // the resulting operandType is nullable if that was introduced via cast. operandType = ClassifyAndVisitConversion( conversionOpt ?? conversionOperand, targetTypeWithNullability, operandType, useLegacyWarnings, assignmentKind, parameterOpt, reportWarnings: reportRemainingWarnings, fromExplicitCast: conversionOpt?.ExplicitCastInCode ?? false, diagnosticLocation); TrackAnalyzedNullabilityThroughConversionGroup(operandType, conversionOpt, conversionOperand); return operandType; } private void SnapshotWalkerThroughConversionGroup(BoundExpression conversionExpression, BoundExpression convertedNode) { if (_snapshotBuilderOpt is null) { return; } var conversionOpt = conversionExpression as BoundConversion; var conversionGroup = conversionOpt?.ConversionGroupOpt; while (conversionOpt != null && conversionOpt != convertedNode && conversionOpt.Syntax.SpanStart != convertedNode.Syntax.SpanStart) { Debug.Assert(conversionOpt.ConversionGroupOpt == conversionGroup); TakeIncrementalSnapshot(conversionOpt); conversionOpt = conversionOpt.Operand as BoundConversion; } } private void TrackAnalyzedNullabilityThroughConversionGroup(TypeWithState resultType, BoundConversion? conversionOpt, BoundExpression convertedNode) { var visitResult = new VisitResult(resultType, resultType.ToTypeWithAnnotations(compilation)); var conversionGroup = conversionOpt?.ConversionGroupOpt; while (conversionOpt != null && conversionOpt != convertedNode) { Debug.Assert(conversionOpt.ConversionGroupOpt == conversionGroup); visitResult = withType(visitResult, conversionOpt.Type); SetAnalyzedNullability(conversionOpt, visitResult); conversionOpt = conversionOpt.Operand as BoundConversion; } static VisitResult withType(VisitResult visitResult, TypeSymbol newType) => new VisitResult(TypeWithState.Create(newType, visitResult.RValueType.State), TypeWithAnnotations.Create(newType, visitResult.LValueType.NullableAnnotation)); } /// /// Return the return type for a lifted operator, given the nullability state of its operands. /// private TypeWithState GetLiftedReturnType(TypeWithAnnotations returnType, NullableFlowState operandState) { bool typeNeedsLifting = returnType.Type.IsNonNullableValueType(); TypeSymbol type = typeNeedsLifting ? MakeNullableOf(returnType) : returnType.Type; NullableFlowState state = returnType.ToTypeWithState().State.Join(operandState); return TypeWithState.Create(type, state); } private static TypeWithState GetNullableUnderlyingTypeIfNecessary(bool isLifted, TypeWithState typeWithState) { if (isLifted) { var type = typeWithState.Type; if (type?.IsNullableType() == true) { return type.GetNullableUnderlyingTypeWithAnnotations().ToTypeWithState(); } } return typeWithState; } private TypeWithState GetLiftedReturnTypeIfNecessary(bool isLifted, TypeWithAnnotations returnType, NullableFlowState operandState) { return isLifted ? GetLiftedReturnType(returnType, operandState) : returnType.ToTypeWithState(); } private TypeSymbol MakeNullableOf(TypeWithAnnotations underlying) { return compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(ImmutableArray.Create(underlying)); } private TypeWithState ClassifyAndVisitConversion( BoundExpression node, TypeWithAnnotations targetType, TypeWithState operandType, bool useLegacyWarnings, AssignmentKind assignmentKind, ParameterSymbol? parameterOpt, bool reportWarnings, bool fromExplicitCast, Location diagnosticLocation) { Debug.Assert(operandType.Type is object); Debug.Assert(diagnosticLocation != null); HashSet? useSiteDiagnostics = null; var conversion = _conversions.ClassifyStandardConversion(null, operandType.Type, targetType.Type, ref useSiteDiagnostics); if (reportWarnings && !conversion.Exists) { if (assignmentKind == AssignmentKind.Argument) { ReportNullabilityMismatchInArgument(diagnosticLocation, operandType.Type, parameterOpt, targetType.Type, forOutput: false); } else { ReportNullabilityMismatchInAssignment(diagnosticLocation, operandType.Type, targetType.Type); } } return VisitConversion( conversionOpt: null, conversionOperand: node, conversion, targetType, operandType, checkConversion: false, fromExplicitCast, useLegacyWarnings: useLegacyWarnings, assignmentKind, parameterOpt, reportTopLevelWarnings: reportWarnings, reportRemainingWarnings: !fromExplicitCast && reportWarnings, diagnosticLocationOpt: diagnosticLocation); } public override BoundNode? VisitDelegateCreationExpression(BoundDelegateCreationExpression node) { Debug.Assert(node.Type.IsDelegateType()); if (node.MethodOpt?.OriginalDefinition is LocalFunctionSymbol localFunc) { VisitLocalFunctionUse(localFunc); } var delegateType = (NamedTypeSymbol)node.Type; switch (node.Argument) { case BoundMethodGroup group: { VisitMethodGroup(group); var method = node.MethodOpt; if (method is object) { method = CheckMethodGroupReceiverNullability(group, delegateType.DelegateInvokeMethod.Parameters, method, node.IsExtensionMethod); if (!group.IsSuppressed) { ReportNullabilityMismatchWithTargetDelegate(group.Syntax.Location, delegateType, delegateType.DelegateInvokeMethod, method, node.IsExtensionMethod); } } SetAnalyzedNullability(group, default); } break; case BoundLambda lambda: { VisitLambda(lambda, delegateType); SetNotNullResult(lambda); if (!lambda.IsSuppressed) { ReportNullabilityMismatchWithTargetDelegate(lambda.Symbol.DiagnosticLocation, delegateType, lambda.UnboundLambda); } } break; case BoundExpression arg when arg.Type is { TypeKind: TypeKind.Delegate } argType: { var argTypeWithAnnotations = TypeWithAnnotations.Create(argType, NullableAnnotation.NotAnnotated); var argState = VisitRvalueWithState(arg); ReportNullableAssignmentIfNecessary(arg, argTypeWithAnnotations, argState, useLegacyWarnings: false); if (!arg.IsSuppressed) { ReportNullabilityMismatchWithTargetDelegate(arg.Syntax.Location, delegateType, delegateType.DelegateInvokeMethod, argType.DelegateInvokeMethod(), invokedAsExtensionMethod: false); } // Delegate creation will throw an exception if the argument is null LearnFromNonNullTest(arg, ref State); } break; default: VisitRvalue(node.Argument); break; } SetNotNullResult(node); return null; } public override BoundNode? VisitMethodGroup(BoundMethodGroup node) { Debug.Assert(!IsConditionalState); var receiverOpt = node.ReceiverOpt; if (receiverOpt != null) { VisitRvalue(receiverOpt); // Receiver nullability is checked when applying the method group conversion, // when we have a specific method, to avoid reporting null receiver warnings // for extension method delegates. Here, store the receiver state for that check. SetMethodGroupReceiverNullability(receiverOpt, ResultType); } SetNotNullResult(node); return null; } private bool TryGetMethodGroupReceiverNullability([NotNullWhen(true)] BoundExpression? receiverOpt, out TypeWithState type) { if (receiverOpt != null && _methodGroupReceiverMapOpt != null && _methodGroupReceiverMapOpt.TryGetValue(receiverOpt, out type)) { return true; } else { type = default; return false; } } private void SetMethodGroupReceiverNullability(BoundExpression receiver, TypeWithState type) { _methodGroupReceiverMapOpt ??= PooledDictionary.GetInstance(); _methodGroupReceiverMapOpt[receiver] = type; } private MethodSymbol CheckMethodGroupReceiverNullability(BoundMethodGroup group, ImmutableArray parameters, MethodSymbol method, bool invokedAsExtensionMethod) { var receiverOpt = group.ReceiverOpt; if (TryGetMethodGroupReceiverNullability(receiverOpt, out TypeWithState receiverType)) { var syntax = group.Syntax; if (!invokedAsExtensionMethod) { method = (MethodSymbol)AsMemberOfType(receiverType.Type, method); } if (method.IsGenericMethod && HasImplicitTypeArguments(group.Syntax)) { var arguments = ArrayBuilder.GetInstance(); if (invokedAsExtensionMethod) { arguments.Add(CreatePlaceholderIfNecessary(receiverOpt, receiverType.ToTypeWithAnnotations(compilation))); } // Create placeholders for the arguments. (See Conversions.GetDelegateArguments() // which is used for that purpose in initial binding.) foreach (var parameter in parameters) { var parameterType = parameter.TypeWithAnnotations; arguments.Add(new BoundExpressionWithNullability(syntax, new BoundParameter(syntax, parameter), parameterType.NullableAnnotation, parameterType.Type)); } Debug.Assert(_binder is object); method = InferMethodTypeArguments(_binder, method, arguments.ToImmutableAndFree(), argumentRefKindsOpt: default, argsToParamsOpt: default, expanded: false); } if (invokedAsExtensionMethod) { CheckExtensionMethodThisNullability(receiverOpt, Conversion.Identity, method.Parameters[0], receiverType); } else { CheckPossibleNullReceiver(receiverOpt, receiverType, checkNullableValueType: false); } if (ConstraintsHelper.RequiresChecking(method)) { CheckMethodConstraints(syntax, method); } } return method; } public override BoundNode? VisitLambda(BoundLambda node) { // Note: actual lambda analysis happens after this call (primarily in VisitConversion). // Here we just indicate that a lambda expression produces a non-null value. SetNotNullResult(node); return null; } private void VisitLambda(BoundLambda node, NamedTypeSymbol? delegateTypeOpt, Optional initialState = default) { Debug.Assert(delegateTypeOpt?.IsDelegateType() != false); var delegateInvokeMethod = delegateTypeOpt?.DelegateInvokeMethod; bool useDelegateInvokeParameterTypes = UseDelegateInvokeParameterTypes(node, delegateInvokeMethod); if (useDelegateInvokeParameterTypes && _snapshotBuilderOpt is object) { SetUpdatedSymbol(node, node.Symbol, delegateTypeOpt!); } AnalyzeLocalFunctionOrLambda( node, node.Symbol, initialState.HasValue ? initialState.Value : State.Clone(), delegateInvokeMethod, useDelegateInvokeParameterTypes); } private static bool UseDelegateInvokeParameterTypes(BoundLambda lambda, MethodSymbol? delegateInvokeMethod) { return delegateInvokeMethod is object && !lambda.UnboundLambda.HasExplicitlyTypedParameterList; } public override BoundNode? VisitUnboundLambda(UnboundLambda node) { // The presence of this node suggests an error was detected in an earlier phase. // Analyze the body to report any additional warnings. var lambda = node.BindForErrorRecovery(); VisitLambda(lambda, delegateTypeOpt: null); SetNotNullResult(node); return null; } public override BoundNode? VisitThisReference(BoundThisReference node) { VisitThisOrBaseReference(node); return null; } private void VisitThisOrBaseReference(BoundExpression node) { var rvalueResult = TypeWithState.Create(node.Type, NullableFlowState.NotNull); var lvalueResult = TypeWithAnnotations.Create(node.Type, NullableAnnotation.NotAnnotated); SetResult(node, rvalueResult, lvalueResult); } public override BoundNode? VisitParameter(BoundParameter node) { var parameter = node.ParameterSymbol; int slot = GetOrCreateSlot(parameter); var parameterType = GetDeclaredParameterResult(parameter); var typeWithState = GetParameterState(parameterType, parameter.FlowAnalysisAnnotations); SetResult(node, GetAdjustedResult(typeWithState, slot), parameterType); return null; } public override BoundNode? VisitAssignmentOperator(BoundAssignmentOperator node) { Debug.Assert(!IsConditionalState); var left = node.Left; switch (left) { // when binding initializers, we treat assignments to auto-properties or field-like events as direct assignments to the underlying field. // in order to track member state based on these initializers, we need to see the assignment in terms of the associated member case BoundFieldAccess { ExpressionSymbol: FieldSymbol { AssociatedSymbol: PropertySymbol autoProperty } } fieldAccess: left = new BoundPropertyAccess(fieldAccess.Syntax, fieldAccess.ReceiverOpt, autoProperty, LookupResultKind.Viable, autoProperty.Type, fieldAccess.HasErrors); break; case BoundFieldAccess { ExpressionSymbol: FieldSymbol { AssociatedSymbol: EventSymbol @event } } fieldAccess: left = new BoundEventAccess(fieldAccess.Syntax, fieldAccess.ReceiverOpt, @event, isUsableAsField: true, LookupResultKind.Viable, @event.Type, fieldAccess.HasErrors); break; } var right = node.Right; VisitLValue(left); // we may enter a conditional state for error scenarios on the LHS. Unsplit(); FlowAnalysisAnnotations leftAnnotations = GetLValueAnnotations(left); TypeWithAnnotations declaredType = LvalueResultType; TypeWithAnnotations leftLValueType = ApplyLValueAnnotations(declaredType, leftAnnotations); if (left.Kind == BoundKind.EventAccess && ((BoundEventAccess)left).EventSymbol.IsWindowsRuntimeEvent) { // Event assignment is a call to an Add method. (Note that assignment // of non-field-like events uses BoundEventAssignmentOperator // rather than BoundAssignmentOperator.) VisitRvalue(right); SetNotNullResult(node); } else { TypeWithState rightState; if (!node.IsRef) { var discarded = left is BoundDiscardExpression; rightState = VisitOptionalImplicitConversion(right, targetTypeOpt: discarded ? default : leftLValueType, UseLegacyWarnings(left, leftLValueType), trackMembers: true, AssignmentKind.Assignment); } else { rightState = VisitRefExpression(right, leftLValueType); } // If the LHS has annotations, we perform an additional check for nullable value types CheckDisallowedNullAssignment(rightState, leftAnnotations, right.Syntax.Location); AdjustSetValue(left, declaredType, leftLValueType, ref rightState); TrackNullableStateForAssignment(right, leftLValueType, MakeSlot(left), rightState, MakeSlot(right)); if (left is BoundDiscardExpression) { var lvalueType = rightState.ToTypeWithAnnotations(compilation); SetResult(left, rightState, lvalueType, isLvalue: true); SetResult(node, rightState, lvalueType); } else { SetResult(node, TypeWithState.Create(leftLValueType.Type, rightState.State), leftLValueType); } } return null; } /// /// When the allowed output of a property/indexer is not-null but the allowed input is maybe-null, we store a not-null value instead. /// This way, assignment of a legal input value results in a legal output value. /// This adjustment doesn't apply to oblivious properties/indexers. /// private void AdjustSetValue(BoundExpression left, TypeWithAnnotations declaredType, TypeWithAnnotations leftLValueType, ref TypeWithState rightState) { if ((left is BoundPropertyAccess || left is BoundIndexerAccess) && !declaredType.NullableAnnotation.IsOblivious() && isAllowedOutputStricter(leftLValueType, declaredType, getRValueAnnotations(left))) { rightState = rightState.WithNotNullState(); } return; static bool isAllowedOutputStricter(TypeWithAnnotations allowedInput, TypeWithAnnotations declaredType, FlowAnalysisAnnotations outputAnnotations) { if (!allowedInput.CanBeAssignedNull) { // allowed input is `!`, ie. stricter return false; } var allowedOutput = ApplyUnconditionalAnnotations(declaredType.ToTypeWithState(), outputAnnotations); return allowedOutput.IsNotNull; } FlowAnalysisAnnotations getRValueAnnotations(BoundExpression expr) { return expr switch { BoundPropertyAccess property => GetRValueAnnotations(property.PropertySymbol), BoundIndexerAccess indexer => GetRValueAnnotations(indexer.Indexer), _ => throw ExceptionUtilities.UnexpectedValue(expr.Kind) }; } } private FlowAnalysisAnnotations GetLValueAnnotations(BoundExpression expr) { // Annotations are ignored when binding an attribute to avoid cycles. (Members used // in attributes are error scenarios, so missing warnings should not be important.) if (IsAnalyzingAttribute) { return FlowAnalysisAnnotations.None; } var annotations = expr switch { BoundPropertyAccess property => property.PropertySymbol.GetFlowAnalysisAnnotations(), BoundIndexerAccess indexer => indexer.Indexer.GetFlowAnalysisAnnotations(), BoundFieldAccess field => getFieldAnnotations(field.FieldSymbol), BoundObjectInitializerMember { MemberSymbol: PropertySymbol prop } => prop.GetFlowAnalysisAnnotations(), BoundObjectInitializerMember { MemberSymbol: FieldSymbol field } => getFieldAnnotations(field), BoundParameter { ParameterSymbol: ParameterSymbol parameter } => ToInwardAnnotations(GetParameterAnnotations(parameter) & ~FlowAnalysisAnnotations.NotNull), // NotNull is enforced upon method exit _ => FlowAnalysisAnnotations.None }; return annotations & (FlowAnalysisAnnotations.DisallowNull | FlowAnalysisAnnotations.AllowNull); static FlowAnalysisAnnotations getFieldAnnotations(FieldSymbol field) { return field.AssociatedSymbol is PropertySymbol property ? property.GetFlowAnalysisAnnotations() : field.FlowAnalysisAnnotations; } } private static FlowAnalysisAnnotations ToInwardAnnotations(FlowAnalysisAnnotations outwardAnnotations) { var annotations = FlowAnalysisAnnotations.None; if ((outwardAnnotations & FlowAnalysisAnnotations.MaybeNull) != 0) { // MaybeNull and MaybeNullWhen count as MaybeNull annotations |= FlowAnalysisAnnotations.AllowNull; } if ((outwardAnnotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull) { // NotNullWhenTrue and NotNullWhenFalse don't count on their own. Only NotNull (ie. both flags) matters. annotations |= FlowAnalysisAnnotations.DisallowNull; } return annotations; } private static bool UseLegacyWarnings(BoundExpression expr, TypeWithAnnotations exprType) { switch (expr.Kind) { case BoundKind.Local: return expr.GetRefKind() == RefKind.None; case BoundKind.Parameter: RefKind kind = ((BoundParameter)expr).ParameterSymbol.RefKind; return kind == RefKind.None; default: return false; } } public override BoundNode? VisitDeconstructionAssignmentOperator(BoundDeconstructionAssignmentOperator node) { return VisitDeconstructionAssignmentOperator(node, rightResultOpt: null); } private BoundNode? VisitDeconstructionAssignmentOperator(BoundDeconstructionAssignmentOperator node, TypeWithState? rightResultOpt) { var previousDisableNullabilityAnalysis = _disableNullabilityAnalysis; _disableNullabilityAnalysis = true; var left = node.Left; var right = node.Right; var variables = GetDeconstructionAssignmentVariables(left); if (node.HasErrors) { // In the case of errors, simply visit the right as an r-value to update // any nullability state even though deconstruction is skipped. VisitRvalue(right.Operand); } else { VisitDeconstructionArguments(variables, right.Conversion, right.Operand, rightResultOpt); } variables.FreeAll(v => v.NestedVariables); // https://github.com/dotnet/roslyn/issues/33011: Result type should be inferred and the constraints should // be re-verified. Even though the standard tuple type has no constraints we support that scenario. Constraints_78 // has a test for this case that should start failing when this is fixed. SetNotNullResult(node); _disableNullabilityAnalysis = previousDisableNullabilityAnalysis; return null; } private void VisitDeconstructionArguments(ArrayBuilder variables, Conversion conversion, BoundExpression right, TypeWithState? rightResultOpt = null) { Debug.Assert(conversion.Kind == ConversionKind.Deconstruction); if (!conversion.DeconstructionInfo.IsDefault) { VisitDeconstructMethodArguments(variables, conversion, right, rightResultOpt); } else { VisitTupleDeconstructionArguments(variables, conversion.UnderlyingConversions, right); } } private void VisitDeconstructMethodArguments(ArrayBuilder variables, Conversion conversion, BoundExpression right, TypeWithState? rightResultOpt) { VisitRvalue(right); // If we were passed an explicit right result, use that rather than the visited result if (rightResultOpt.HasValue) { SetResultType(right, rightResultOpt.Value); } var rightResult = ResultType; var invocation = conversion.DeconstructionInfo.Invocation as BoundCall; var deconstructMethod = invocation?.Method; if (deconstructMethod is object) { Debug.Assert(invocation is object); Debug.Assert(invocation.BinderOpt is object); Debug.Assert(rightResult.Type is object); int n = variables.Count; if (!invocation.InvokedAsExtensionMethod) { _ = CheckPossibleNullReceiver(right); // update the deconstruct method with any inferred type parameters of the containing type if (deconstructMethod.OriginalDefinition != deconstructMethod) { deconstructMethod = deconstructMethod.OriginalDefinition.AsMember((NamedTypeSymbol)rightResult.Type); } } else { if (deconstructMethod.IsGenericMethod) { // re-infer the deconstruct parameters based on the 'this' parameter ArrayBuilder placeholderArgs = ArrayBuilder.GetInstance(n + 1); placeholderArgs.Add(CreatePlaceholderIfNecessary(right, rightResult.ToTypeWithAnnotations(compilation))); for (int i = 0; i < n; i++) { placeholderArgs.Add(new BoundExpressionWithNullability(variables[i].Expression.Syntax, variables[i].Expression, NullableAnnotation.Oblivious, conversion.DeconstructionInfo.OutputPlaceholders[i].Type)); } deconstructMethod = InferMethodTypeArguments(invocation.BinderOpt, deconstructMethod, placeholderArgs.ToImmutableAndFree(), invocation.ArgumentRefKindsOpt, invocation.ArgsToParamsOpt, invocation.Expanded); // check the constraints remain valid with the re-inferred parameter types if (ConstraintsHelper.RequiresChecking(deconstructMethod)) { CheckMethodConstraints(invocation.Syntax, deconstructMethod); } } } var parameters = deconstructMethod.Parameters; int offset = invocation.InvokedAsExtensionMethod ? 1 : 0; Debug.Assert(parameters.Length - offset == n); if (invocation.InvokedAsExtensionMethod) { // Check nullability for `this` parameter var argConversion = RemoveConversion(invocation.Arguments[0], includeExplicitConversions: false).conversion; CheckExtensionMethodThisNullability(right, argConversion, deconstructMethod.Parameters[0], rightResult); } for (int i = 0; i < n; i++) { var variable = variables[i]; var parameter = parameters[i + offset]; var underlyingConversion = conversion.UnderlyingConversions[i]; var nestedVariables = variable.NestedVariables; if (nestedVariables != null) { var nestedRight = CreatePlaceholderIfNecessary(invocation.Arguments[i + offset], parameter.TypeWithAnnotations); VisitDeconstructionArguments(nestedVariables, underlyingConversion, right: nestedRight); } else { VisitArgumentConversionAndInboundAssignmentsAndPreConditions(conversionOpt: null, variable.Expression, underlyingConversion, parameter.RefKind, parameter, parameter.TypeWithAnnotations, GetParameterAnnotations(parameter), new VisitArgumentResult(new VisitResult(variable.Type.ToTypeWithState(), variable.Type), stateForLambda: default), extensionMethodThisArgument: false); } } for (int i = 0; i < n; i++) { var variable = variables[i]; var parameter = parameters[i + offset]; var nestedVariables = variable.NestedVariables; if (nestedVariables == null) { VisitArgumentOutboundAssignmentsAndPostConditions( variable.Expression, parameter.RefKind, parameter, parameter.TypeWithAnnotations, GetRValueAnnotations(parameter), new VisitArgumentResult(new VisitResult(variable.Type.ToTypeWithState(), variable.Type), stateForLambda: default), notNullParametersOpt: null, compareExchangeInfoOpt: default); } } } } private void VisitTupleDeconstructionArguments(ArrayBuilder variables, ImmutableArray conversions, BoundExpression right) { int n = variables.Count; var rightParts = GetDeconstructionRightParts(right); Debug.Assert(rightParts.Length == n); for (int i = 0; i < n; i++) { var variable = variables[i]; var underlyingConversion = conversions[i]; var rightPart = rightParts[i]; var nestedVariables = variable.NestedVariables; if (nestedVariables != null) { VisitDeconstructionArguments(nestedVariables, underlyingConversion, rightPart); } else { var lvalueType = variable.Type; var leftAnnotations = GetLValueAnnotations(variable.Expression); lvalueType = ApplyLValueAnnotations(lvalueType, leftAnnotations); TypeWithState operandType; TypeWithState valueType; int valueSlot; if (underlyingConversion.IsIdentity) { if (variable.Expression is BoundLocal { DeclarationKind: BoundLocalDeclarationKind.WithInferredType } local) { // when the LHS is a var declaration, we can just visit the right part to infer the type valueType = operandType = VisitRvalueWithState(rightPart); _variableTypes[local.LocalSymbol] = operandType.ToAnnotatedTypeWithAnnotations(compilation); } else { operandType = default; valueType = VisitOptionalImplicitConversion(rightPart, lvalueType, useLegacyWarnings: true, trackMembers: true, AssignmentKind.Assignment); } valueSlot = MakeSlot(rightPart); } else { operandType = VisitRvalueWithState(rightPart); valueType = VisitConversion( conversionOpt: null, rightPart, underlyingConversion, lvalueType, operandType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: true, AssignmentKind.Assignment, reportTopLevelWarnings: true, reportRemainingWarnings: true, trackMembers: false); valueSlot = -1; } // If the LHS has annotations, we perform an additional check for nullable value types CheckDisallowedNullAssignment(valueType, leftAnnotations, right.Syntax.Location); int targetSlot = MakeSlot(variable.Expression); AdjustSetValue(variable.Expression, variable.Type, lvalueType, ref valueType); TrackNullableStateForAssignment(rightPart, lvalueType, targetSlot, valueType, valueSlot); // Conversion of T to Nullable is equivalent to new Nullable(t). if (targetSlot > 0 && underlyingConversion.Kind == ConversionKind.ImplicitNullable && AreNullableAndUnderlyingTypes(lvalueType.Type, operandType.Type, out TypeWithAnnotations underlyingType)) { valueSlot = MakeSlot(rightPart); if (valueSlot > 0) { var valueBeforeNullableWrapping = TypeWithState.Create(underlyingType.Type, NullableFlowState.NotNull); TrackNullableStateOfNullableValue(targetSlot, lvalueType.Type, rightPart, valueBeforeNullableWrapping, valueSlot); } } } } } private readonly struct DeconstructionVariable { internal readonly BoundExpression Expression; internal readonly TypeWithAnnotations Type; internal readonly ArrayBuilder? NestedVariables; internal DeconstructionVariable(BoundExpression expression, TypeWithAnnotations type) { Expression = expression; Type = type; NestedVariables = null; } internal DeconstructionVariable(BoundExpression expression, ArrayBuilder nestedVariables) { Expression = expression; Type = default; NestedVariables = nestedVariables; } } private ArrayBuilder GetDeconstructionAssignmentVariables(BoundTupleExpression tuple) { var arguments = tuple.Arguments; var builder = ArrayBuilder.GetInstance(arguments.Length); foreach (var argument in arguments) { builder.Add(getDeconstructionAssignmentVariable(argument)); } return builder; DeconstructionVariable getDeconstructionAssignmentVariable(BoundExpression expr) { switch (expr.Kind) { case BoundKind.TupleLiteral: case BoundKind.ConvertedTupleLiteral: return new DeconstructionVariable(expr, GetDeconstructionAssignmentVariables((BoundTupleExpression)expr)); default: VisitLValue(expr); return new DeconstructionVariable(expr, LvalueResultType); } } } /// /// Return the sub-expressions for the righthand side of a deconstruction /// assignment. cf. LocalRewriter.GetRightParts. /// private ImmutableArray GetDeconstructionRightParts(BoundExpression expr) { switch (expr.Kind) { case BoundKind.TupleLiteral: case BoundKind.ConvertedTupleLiteral: return ((BoundTupleExpression)expr).Arguments; case BoundKind.Conversion: { var conv = (BoundConversion)expr; switch (conv.ConversionKind) { case ConversionKind.Identity: case ConversionKind.ImplicitTupleLiteral: return GetDeconstructionRightParts(conv.Operand); } } break; } if (expr.Type is NamedTypeSymbol { IsTupleType: true } tupleType) { // https://github.com/dotnet/roslyn/issues/33011: Should include conversion.UnderlyingConversions[i]. // For instance, Boxing conversions (see Deconstruction_ImplicitBoxingConversion_02) and // ImplicitNullable conversions (see Deconstruction_ImplicitNullableConversion_02). var fields = tupleType.TupleElements; return fields.SelectAsArray((f, e) => (BoundExpression)new BoundFieldAccess(e.Syntax, e, f, constantValueOpt: null), expr); } throw ExceptionUtilities.Unreachable; } public override BoundNode? VisitIncrementOperator(BoundIncrementOperator node) { Debug.Assert(!IsConditionalState); var operandType = VisitRvalueWithState(node.Operand); var operandLvalue = LvalueResultType; bool setResult = false; if (this.State.Reachable) { // https://github.com/dotnet/roslyn/issues/29961 Update increment method based on operand type. MethodSymbol? incrementOperator = (node.OperatorKind.IsUserDefined() && node.MethodOpt?.ParameterCount == 1) ? node.MethodOpt : null; TypeWithAnnotations targetTypeOfOperandConversion; AssignmentKind assignmentKind = AssignmentKind.Assignment; ParameterSymbol? parameter = null; // Analyze operator call properly (honoring [Disallow|Allow|Maybe|NotNull] attribute annotations) https://github.com/dotnet/roslyn/issues/32671 // https://github.com/dotnet/roslyn/issues/29961 Update conversion method based on operand type. if (node.OperandConversion.IsUserDefined && node.OperandConversion.Method?.ParameterCount == 1) { targetTypeOfOperandConversion = node.OperandConversion.Method.ReturnTypeWithAnnotations; } else if (incrementOperator is object) { targetTypeOfOperandConversion = incrementOperator.Parameters[0].TypeWithAnnotations; assignmentKind = AssignmentKind.Argument; parameter = incrementOperator.Parameters[0]; } else { // Either a built-in increment, or an error case. targetTypeOfOperandConversion = default; } TypeWithState resultOfOperandConversionType; if (targetTypeOfOperandConversion.HasType) { // https://github.com/dotnet/roslyn/issues/29961 Should something special be done for targetTypeOfOperandConversion for lifted case? resultOfOperandConversionType = VisitConversion( conversionOpt: null, node.Operand, node.OperandConversion, targetTypeOfOperandConversion, operandType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, assignmentKind, parameter, reportTopLevelWarnings: true, reportRemainingWarnings: true); } else { resultOfOperandConversionType = operandType; } TypeWithState resultOfIncrementType; if (incrementOperator is null) { resultOfIncrementType = resultOfOperandConversionType; } else { resultOfIncrementType = incrementOperator.ReturnTypeWithAnnotations.ToTypeWithState(); } var operandTypeWithAnnotations = operandType.ToTypeWithAnnotations(compilation); resultOfIncrementType = VisitConversion( conversionOpt: null, node, node.ResultConversion, operandTypeWithAnnotations, resultOfIncrementType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment); // https://github.com/dotnet/roslyn/issues/29961 Check node.Type.IsErrorType() instead? if (!node.HasErrors) { var op = node.OperatorKind.Operator(); TypeWithState resultType = (op == UnaryOperatorKind.PrefixIncrement || op == UnaryOperatorKind.PrefixDecrement) ? resultOfIncrementType : operandType; SetResultType(node, resultType); setResult = true; TrackNullableStateForAssignment(node, targetType: operandLvalue, targetSlot: MakeSlot(node.Operand), valueType: resultOfIncrementType); } } if (!setResult) { SetNotNullResult(node); } return null; } public override BoundNode? VisitCompoundAssignmentOperator(BoundCompoundAssignmentOperator node) { var left = node.Left; var right = node.Right; Visit(left); TypeWithAnnotations declaredType = LvalueResultType; TypeWithAnnotations leftLValueType = declaredType; TypeWithState leftResultType = ResultType; Debug.Assert(!IsConditionalState); TypeWithState leftOnRightType = GetAdjustedResult(leftResultType, MakeSlot(node.Left)); // https://github.com/dotnet/roslyn/issues/29962 Update operator based on inferred argument types. if ((object)node.Operator.LeftType != null) { // https://github.com/dotnet/roslyn/issues/29962 Ignoring top-level nullability of operator left parameter. leftOnRightType = VisitConversion( conversionOpt: null, node.Left, node.LeftConversion, TypeWithAnnotations.Create(node.Operator.LeftType), leftOnRightType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment, reportTopLevelWarnings: false, reportRemainingWarnings: true); } else { leftOnRightType = default; } TypeWithState resultType; TypeWithState rightType = VisitRvalueWithState(right); if ((object)node.Operator.ReturnType != null) { if (node.Operator.Kind.IsUserDefined() && (object)node.Operator.Method != null && node.Operator.Method.ParameterCount == 2) { MethodSymbol method = node.Operator.Method; VisitArguments(node, ImmutableArray.Create(node.Left, right), method.ParameterRefKinds, method.Parameters, argsToParamsOpt: default, expanded: true, invokedAsExtensionMethod: false, method); } resultType = InferResultNullability(node.Operator.Kind, node.Operator.Method, node.Operator.ReturnType, leftOnRightType, rightType); FlowAnalysisAnnotations leftAnnotations = GetLValueAnnotations(node.Left); leftLValueType = ApplyLValueAnnotations(leftLValueType, leftAnnotations); resultType = VisitConversion( conversionOpt: null, node, node.FinalConversion, leftLValueType, resultType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment); // If the LHS has annotations, we perform an additional check for nullable value types CheckDisallowedNullAssignment(resultType, leftAnnotations, node.Syntax.Location); } else { resultType = TypeWithState.Create(node.Type, NullableFlowState.NotNull); } AdjustSetValue(left, declaredType, leftLValueType, ref resultType); TrackNullableStateForAssignment(node, leftLValueType, MakeSlot(node.Left), resultType); SetResultType(node, resultType); return null; } public override BoundNode? VisitFixedLocalCollectionInitializer(BoundFixedLocalCollectionInitializer node) { var initializer = node.Expression; if (initializer.Kind == BoundKind.AddressOfOperator) { initializer = ((BoundAddressOfOperator)initializer).Operand; } VisitRvalue(initializer); if (node.Expression.Kind == BoundKind.AddressOfOperator) { SetResultType(node.Expression, TypeWithState.Create(node.Expression.Type, ResultType.State)); } SetNotNullResult(node); return null; } public override BoundNode? VisitAddressOfOperator(BoundAddressOfOperator node) { Visit(node.Operand); SetNotNullResult(node); return null; } private void ReportArgumentWarnings(BoundExpression argument, TypeWithState argumentType, ParameterSymbol parameter) { var paramType = parameter.TypeWithAnnotations; ReportNullableAssignmentIfNecessary(argument, paramType, argumentType, useLegacyWarnings: false, AssignmentKind.Argument, parameterOpt: parameter); if (argumentType.Type is { } argType && IsNullabilityMismatch(paramType.Type, argType)) { ReportNullabilityMismatchInArgument(argument.Syntax, argType, parameter, paramType.Type, forOutput: false); } } private void ReportNullabilityMismatchInRefArgument(BoundExpression argument, TypeSymbol argumentType, ParameterSymbol parameter, TypeSymbol parameterType) { ReportDiagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, argument.Syntax, argumentType, parameterType, GetParameterAsDiagnosticArgument(parameter), GetContainingSymbolAsDiagnosticArgument(parameter)); } /// /// Report warning passing argument where nested nullability does not match /// parameter (e.g.: calling `void F(object[] o)` with `F(new[] { maybeNull })`). /// private void ReportNullabilityMismatchInArgument(SyntaxNode argument, TypeSymbol argumentType, ParameterSymbol parameter, TypeSymbol parameterType, bool forOutput) { ReportNullabilityMismatchInArgument(argument.GetLocation(), argumentType, parameter, parameterType, forOutput); } private void ReportNullabilityMismatchInArgument(Location argumentLocation, TypeSymbol argumentType, ParameterSymbol? parameterOpt, TypeSymbol parameterType, bool forOutput) { ReportDiagnostic(forOutput ? ErrorCode.WRN_NullabilityMismatchInArgumentForOutput : ErrorCode.WRN_NullabilityMismatchInArgument, argumentLocation, argumentType, parameterOpt?.Type.IsNonNullableValueType() == true && parameterType.IsNullableType() ? parameterOpt.Type : parameterType, // Compensate for operator lifting GetParameterAsDiagnosticArgument(parameterOpt), GetContainingSymbolAsDiagnosticArgument(parameterOpt)); } private TypeWithAnnotations GetDeclaredLocalResult(LocalSymbol local) { return _variableTypes.TryGetValue(local, out TypeWithAnnotations type) ? type : local.TypeWithAnnotations; } private TypeWithAnnotations GetDeclaredParameterResult(ParameterSymbol parameter) { return _variableTypes.TryGetValue(parameter, out TypeWithAnnotations type) ? type : parameter.TypeWithAnnotations; } public override BoundNode? VisitBaseReference(BoundBaseReference node) { VisitThisOrBaseReference(node); return null; } public override BoundNode? VisitFieldAccess(BoundFieldAccess node) { var updatedSymbol = VisitMemberAccess(node, node.ReceiverOpt, node.FieldSymbol); SetUpdatedSymbol(node, node.FieldSymbol, updatedSymbol); return null; } public override BoundNode? VisitPropertyAccess(BoundPropertyAccess node) { var property = node.PropertySymbol; var updatedMember = VisitMemberAccess(node, node.ReceiverOpt, property); if (!IsAnalyzingAttribute) { if (_expressionIsRead) { ApplyMemberPostConditions(node.ReceiverOpt, property.GetMethod); } else { ApplyMemberPostConditions(node.ReceiverOpt, property.SetMethod); } } SetUpdatedSymbol(node, property, updatedMember); return null; } public override BoundNode? VisitIndexerAccess(BoundIndexerAccess node) { var receiverOpt = node.ReceiverOpt; var receiverType = VisitRvalueWithState(receiverOpt).Type; // 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(receiverOpt); var indexer = node.Indexer; if (receiverType is object) { // Update indexer based on inferred receiver type. indexer = (PropertySymbol)AsMemberOfType(receiverType, indexer); } VisitArguments(node, node.Arguments, node.ArgumentRefKindsOpt, indexer, node.ArgsToParamsOpt, node.Expanded); var resultType = ApplyUnconditionalAnnotations(indexer.TypeWithAnnotations.ToTypeWithState(), GetRValueAnnotations(indexer)); SetResult(node, resultType, indexer.TypeWithAnnotations); SetUpdatedSymbol(node, node.Indexer, indexer); return null; } public override BoundNode? VisitIndexOrRangePatternIndexerAccess(BoundIndexOrRangePatternIndexerAccess node) { BoundExpression receiver = node.Receiver; var receiverType = VisitRvalueWithState(receiver).Type; // 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(receiver); VisitRvalue(node.Argument); var patternSymbol = node.PatternSymbol; if (receiverType is object) { patternSymbol = AsMemberOfType(receiverType, patternSymbol); } SetLvalueResultType(node, patternSymbol.GetTypeOrReturnType()); SetUpdatedSymbol(node, node.PatternSymbol, patternSymbol); return null; } public override BoundNode? VisitEventAccess(BoundEventAccess node) { var updatedSymbol = VisitMemberAccess(node, node.ReceiverOpt, node.EventSymbol); SetUpdatedSymbol(node, node.EventSymbol, updatedSymbol); return null; } private Symbol VisitMemberAccess(BoundExpression node, BoundExpression? receiverOpt, Symbol member) { Debug.Assert(!IsConditionalState); var receiverType = (receiverOpt != null) ? VisitRvalueWithState(receiverOpt) : default; SpecialMember? nullableOfTMember = null; if (member.RequiresInstanceReceiver()) { member = AsMemberOfType(receiverType.Type, member); nullableOfTMember = GetNullableOfTMember(member); // https://github.com/dotnet/roslyn/issues/30598: For l-values, mark receiver as not null // after RHS has been visited, and only if the receiver has not changed. bool skipReceiverNullCheck = nullableOfTMember != SpecialMember.System_Nullable_T_get_Value; _ = CheckPossibleNullReceiver(receiverOpt, checkNullableValueType: !skipReceiverNullCheck); } var type = member.GetTypeOrReturnType(); var memberAnnotations = GetRValueAnnotations(member); var resultType = ApplyUnconditionalAnnotations(type.ToTypeWithState(), memberAnnotations); // We are supposed to track information for the node. Use whatever we managed to // accumulate so far. if (PossiblyNullableType(resultType.Type)) { int slot = MakeMemberSlot(receiverOpt, member); if (slot > 0 && slot < this.State.Capacity) { var state = this.State[slot]; resultType = TypeWithState.Create(resultType.Type, state); } } Debug.Assert(!IsConditionalState); if (nullableOfTMember == SpecialMember.System_Nullable_T_get_HasValue && !(receiverOpt is null)) { int containingSlot = MakeSlot(receiverOpt); if (containingSlot > 0) { Split(); this.StateWhenTrue[containingSlot] = NullableFlowState.NotNull; } } SetResult(node, resultType, type); return member; } private SpecialMember? GetNullableOfTMember(Symbol member) { if (member.Kind == SymbolKind.Property) { var getMethod = ((PropertySymbol)member.OriginalDefinition).GetMethod; if ((object)getMethod != null && getMethod.ContainingType.SpecialType == SpecialType.System_Nullable_T) { if (getMethod == compilation.GetSpecialTypeMember(SpecialMember.System_Nullable_T_get_Value)) { return SpecialMember.System_Nullable_T_get_Value; } if (getMethod == compilation.GetSpecialTypeMember(SpecialMember.System_Nullable_T_get_HasValue)) { return SpecialMember.System_Nullable_T_get_HasValue; } } } return null; } private int GetNullableOfTValueSlot(TypeSymbol containingType, int containingSlot, out Symbol? valueProperty, bool forceSlotEvenIfEmpty = false) { Debug.Assert(containingType.IsNullableType()); Debug.Assert(TypeSymbol.Equals(NominalSlotType(containingSlot), containingType, TypeCompareKind.ConsiderEverything2)); var getValue = (MethodSymbol)compilation.GetSpecialTypeMember(SpecialMember.System_Nullable_T_get_Value); valueProperty = getValue?.AsMember((NamedTypeSymbol)containingType)?.AssociatedSymbol; return (valueProperty is null) ? -1 : GetOrCreateSlot(valueProperty, containingSlot, forceSlotEvenIfEmpty: forceSlotEvenIfEmpty); } protected override void VisitForEachExpression(BoundForEachStatement node) { if (node.Expression.Kind != BoundKind.Conversion) { // If we're in this scenario, there was a binding error, and we should suppress any further warnings. Debug.Assert(node.HasErrors); VisitRvalue(node.Expression); return; } var (expr, conversion) = RemoveConversion(node.Expression, includeExplicitConversions: false); SnapshotWalkerThroughConversionGroup(node.Expression, expr); // There are 7 ways that a foreach can be created: // 1. The collection type is an array type. For this, initial binding will generate an implicit reference conversion to // IEnumerable, and we do not need to do any reinferring of enumerators here. // 2. The collection type is dynamic. For this we do the same as 1. // 3. The collection type implements the GetEnumerator pattern. For this, there is an identity conversion. Because // this identity conversion uses nested types from initial binding, we cannot trust them and must instead use // the type of the expression returned from VisitResult to reinfer the enumerator information. // 4. The collection type implements IEnumerable. Only a few cases can hit this without being caught by number 3, // such as a type with a private implementation of IEnumerable, or a type parameter constrained to that type. // In these cases, there will be an implicit conversion to IEnumerable, but this will use types from // initial binding. For this scenario, we need to look through the list of implemented interfaces on the type and // find the version of IEnumerable that it has after nullable analysis, as type substitution could have changed // nested nullability of type parameters. See ForEach_22 for a concrete example of this. // 5. The collection type implements IEnumerable (non-generic). Because this version isn't generic, we don't need to // do any reinference, and the existing conversion can stand as is. // 6. The target framework's System.String doesn't implement IEnumerable. This is a compat case: System.String normally // does implement IEnumerable, but there are certain target frameworks where this isn't the case. The compiler will // still emit code for foreach in these scenarios. // 7. The collection type implements the GetEnumerator pattern via an extension GetEnumerator. For this, there will be // conversion to the parameter of the extension method. // 8. Some binding error occurred, and some other error has already been reported. Usually this doesn't have any kind // of conversion on top, but if there was an explicit conversion in code then we could get past the initial check // for a BoundConversion node. var resultTypeWithState = VisitRvalueWithState(expr); var resultType = resultTypeWithState.Type; Debug.Assert(resultType is object); SetAnalyzedNullability(expr, _visitResult); TypeWithAnnotations targetTypeWithAnnotations; MethodSymbol? reinferredGetEnumeratorMethod = null; if (node.EnumeratorInfoOpt?.GetEnumeratorMethod is { IsExtensionMethod: true, Parameters: var parameters } enumeratorMethod) { // this is case 7 var (method, results, _) = VisitArguments( node, ImmutableArray.Create(node.Expression), refKindsOpt: default, parameters, argsToParamsOpt: default, expanded: false, invokedAsExtensionMethod: true, enumeratorMethod); targetTypeWithAnnotations = results[0].LValueType; reinferredGetEnumeratorMethod = method; } else if (conversion.IsIdentity || (conversion.Kind == ConversionKind.ExplicitReference && resultType.SpecialType == SpecialType.System_String)) { // This is case 3 or 6. targetTypeWithAnnotations = resultTypeWithState.ToTypeWithAnnotations(compilation); } else if (conversion.IsImplicit) { bool isAsync = node.AwaitOpt != null; if (node.Expression.Type!.SpecialType == SpecialType.System_Collections_IEnumerable) { // If this is a conversion to IEnumerable (non-generic), nothing to do. This is cases 1, 2, and 5. targetTypeWithAnnotations = TypeWithAnnotations.Create(node.Expression.Type); } else if (ForEachLoopBinder.IsIEnumerableT(node.Expression.Type.OriginalDefinition, isAsync, compilation)) { // This is case 4. We need to look for the IEnumerable that this reinferred expression implements, // so that we pick up any nested type substitutions that could have occurred. HashSet? ignoredUseSiteDiagnostics = null; targetTypeWithAnnotations = TypeWithAnnotations.Create(ForEachLoopBinder.GetIEnumerableOfT(resultType, isAsync, compilation, ref ignoredUseSiteDiagnostics, out bool foundMultiple)); Debug.Assert(!foundMultiple); Debug.Assert(targetTypeWithAnnotations.HasType); } else { // This is case 8. There was not a successful binding, as a successful binding will _always_ generate one of the // above conversions. Just return, as we want to suppress further errors. return; } } else { // This is also case 8. return; } var convertedResult = VisitConversion( GetConversionIfApplicable(node.Expression, expr), expr, conversion, targetTypeWithAnnotations, resultTypeWithState, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, AssignmentKind.Assignment); bool reportedDiagnostic = node.EnumeratorInfoOpt?.GetEnumeratorMethod is { IsExtensionMethod: true } ? false : CheckPossibleNullReceiver(expr); SetAnalyzedNullability(node.Expression, new VisitResult(convertedResult, convertedResult.ToTypeWithAnnotations(compilation))); TypeWithState currentPropertyGetterTypeWithState; if (node.EnumeratorInfoOpt is null) { currentPropertyGetterTypeWithState = default; } else if (resultType is ArrayTypeSymbol arrayType) { // Even though arrays use the IEnumerator pattern, we use the array element type as the foreach target type, so // directly get our source type from there instead of doing method reinference. currentPropertyGetterTypeWithState = arrayType.ElementTypeWithAnnotations.ToTypeWithState(); } else if (resultType.SpecialType == SpecialType.System_String) { // There are frameworks where System.String does not implement IEnumerable, but we still lower it to a for loop // using the indexer over the individual characters anyway. So the type must be not annotated char. currentPropertyGetterTypeWithState = TypeWithAnnotations.Create(node.EnumeratorInfoOpt.ElementType, NullableAnnotation.NotAnnotated).ToTypeWithState(); } else { // Reinfer the return type of the node.Expression.GetEnumerator().Current property, so that if // the collection changed nested generic types we pick up those changes. reinferredGetEnumeratorMethod ??= (MethodSymbol)AsMemberOfType(convertedResult.Type, node.EnumeratorInfoOpt.GetEnumeratorMethod); var enumeratorReturnType = GetReturnTypeWithState(reinferredGetEnumeratorMethod); if (enumeratorReturnType.State != NullableFlowState.NotNull) { if (!reportedDiagnostic && !(node.Expression is BoundConversion { Operand: { IsSuppressed: true } })) { ReportDiagnostic(ErrorCode.WRN_NullReferenceReceiver, expr.Syntax.GetLocation()); } } var currentPropertyGetter = (MethodSymbol)AsMemberOfType(enumeratorReturnType.Type, node.EnumeratorInfoOpt.CurrentPropertyGetter); currentPropertyGetterTypeWithState = ApplyUnconditionalAnnotations( currentPropertyGetter.ReturnTypeWithAnnotations.ToTypeWithState(), currentPropertyGetter.ReturnTypeFlowAnalysisAnnotations); } SetResultType(expression: null, currentPropertyGetterTypeWithState); } public override void VisitForEachIterationVariables(BoundForEachStatement node) { // ResultType should have been set by VisitForEachExpression, called just before this. var sourceState = node.EnumeratorInfoOpt == null ? default : ResultType; TypeWithAnnotations sourceType = sourceState.ToTypeWithAnnotations(compilation); #pragma warning disable IDE0055 // Fix formatting var variableLocation = node.Syntax switch { ForEachStatementSyntax statement => statement.Identifier.GetLocation(), ForEachVariableStatementSyntax variableStatement => variableStatement.Variable.GetLocation(), _ => throw ExceptionUtilities.UnexpectedValue(node.Syntax) }; #pragma warning restore IDE0055 // Fix formatting if (node.DeconstructionOpt is object) { var assignment = node.DeconstructionOpt.DeconstructionAssignment; // Visit the assignment as a deconstruction with an explicit type VisitDeconstructionAssignmentOperator(assignment, sourceState.HasNullType ? (TypeWithState?)null : sourceState); // https://github.com/dotnet/roslyn/issues/35010: if the iteration variable is a tuple deconstruction, we need to put something in the tree Visit(node.IterationVariableType); } else { Visit(node.IterationVariableType); foreach (var iterationVariable in node.IterationVariables) { var state = NullableFlowState.NotNull; if (!sourceState.HasNullType) { TypeWithAnnotations destinationType = iterationVariable.TypeWithAnnotations; TypeWithState result = sourceState; TypeWithState resultForType = sourceState; if (iterationVariable.IsRef) { // foreach (ref DestinationType variable in collection) if (IsNullabilityMismatch(sourceType, destinationType)) { var foreachSyntax = (ForEachStatementSyntax)node.Syntax; ReportNullabilityMismatchInAssignment(foreachSyntax.Type, sourceType, destinationType); } } else if (iterationVariable is SourceLocalSymbol { IsVar: true }) { // foreach (var variable in collection) destinationType = sourceState.ToAnnotatedTypeWithAnnotations(compilation); _variableTypes[iterationVariable] = destinationType; resultForType = destinationType.ToTypeWithState(); } else { // foreach (DestinationType variable in collection) // and asynchronous variants HashSet? useSiteDiagnostics = null; Conversion conversion = node.ElementConversion.Kind == ConversionKind.UnsetConversionKind ? _conversions.ClassifyImplicitConversionFromType(sourceType.Type, destinationType.Type, ref useSiteDiagnostics) : node.ElementConversion; result = VisitConversion( conversionOpt: null, conversionOperand: node.IterationVariableType, conversion, destinationType, sourceState, checkConversion: true, fromExplicitCast: !conversion.IsImplicit, useLegacyWarnings: true, AssignmentKind.ForEachIterationVariable, reportTopLevelWarnings: true, reportRemainingWarnings: true, diagnosticLocationOpt: variableLocation); } // In non-error cases we'll only run this loop a single time. In error cases we'll set the nullability of the VariableType multiple times, but at least end up with something SetAnalyzedNullability(node.IterationVariableType, new VisitResult(resultForType, destinationType), isLvalue: true); state = result.State; } int slot = GetOrCreateSlot(iterationVariable); if (slot > 0) { this.State[slot] = state; } } } } public override BoundNode? VisitFromEndIndexExpression(BoundFromEndIndexExpression node) { var result = base.VisitFromEndIndexExpression(node); SetNotNullResult(node); return result; } public override BoundNode? VisitObjectInitializerMember(BoundObjectInitializerMember node) { // Should be handled by VisitObjectCreationExpression. throw ExceptionUtilities.Unreachable; } public override BoundNode? VisitDynamicObjectInitializerMember(BoundDynamicObjectInitializerMember node) { SetNotNullResult(node); return null; } public override BoundNode? VisitBadExpression(BoundBadExpression node) { var result = base.VisitBadExpression(node); var type = TypeWithAnnotations.Create(node.Type); SetLvalueResultType(node, type); return result; } public override BoundNode? VisitTypeExpression(BoundTypeExpression node) { var result = base.VisitTypeExpression(node); if (node.BoundContainingTypeOpt != null) { VisitTypeExpression(node.BoundContainingTypeOpt); } SetNotNullResult(node); return result; } public override BoundNode? VisitTypeOrValueExpression(BoundTypeOrValueExpression node) { // These should not appear after initial binding except in error cases. var result = base.VisitTypeOrValueExpression(node); SetNotNullResult(node); return result; } public override BoundNode? VisitUnaryOperator(BoundUnaryOperator node) { Debug.Assert(!IsConditionalState); TypeWithState resultType; switch (node.OperatorKind) { case UnaryOperatorKind.BoolLogicalNegation: VisitCondition(node.Operand); SetConditionalState(StateWhenFalse, StateWhenTrue); resultType = adjustForLifting(ResultType); break; case UnaryOperatorKind.DynamicTrue: // We cannot use VisitCondition, because the operand is not of type bool. // Yet we want to keep the result split if it was split. So we simply visit. Visit(node.Operand); resultType = adjustForLifting(ResultType); break; case UnaryOperatorKind.DynamicLogicalNegation: // We cannot use VisitCondition, because the operand is not of type bool. // Yet we want to keep the result split if it was split. So we simply visit. Visit(node.Operand); // If the state is split, the result is `bool` at runtime and we invert it here. if (IsConditionalState) SetConditionalState(StateWhenFalse, StateWhenTrue); resultType = adjustForLifting(ResultType); break; default: if (node.OperatorKind.IsUserDefined() && node.MethodOpt is MethodSymbol method && method.ParameterCount == 1) { var (operand, conversion) = RemoveConversion(node.Operand, includeExplicitConversions: false); VisitRvalue(operand); var operandResult = ResultType; bool isLifted = node.OperatorKind.IsLifted(); var operandType = GetNullableUnderlyingTypeIfNecessary(isLifted, operandResult); // Update method based on inferred operand type. method = (MethodSymbol)AsMemberOfType(operandType.Type!.StrippedType(), method); // Analyze operator call properly (honoring [Disallow|Allow|Maybe|NotNull] attribute annotations) https://github.com/dotnet/roslyn/issues/32671 var parameter = method.Parameters[0]; _ = VisitConversion( node.Operand as BoundConversion, operand, conversion, parameter.TypeWithAnnotations, operandType, checkConversion: true, fromExplicitCast: false, useLegacyWarnings: false, assignmentKind: AssignmentKind.Argument, parameterOpt: parameter); resultType = GetLiftedReturnTypeIfNecessary(isLifted, method.ReturnTypeWithAnnotations, operandResult.State); SetUpdatedSymbol(node, node.MethodOpt, method); } else { VisitRvalue(node.Operand); resultType = adjustForLifting(ResultType); } break; } SetResultType(node, resultType); return null; TypeWithState adjustForLifting(TypeWithState argumentResult) => TypeWithState.Create(node.Type, node.OperatorKind.IsLifted() ? argumentResult.State : NullableFlowState.NotNull); } public override BoundNode? VisitPointerIndirectionOperator(BoundPointerIndirectionOperator node) { var result = base.VisitPointerIndirectionOperator(node); var type = TypeWithAnnotations.Create(node.Type); SetLvalueResultType(node, type); return result; } public override BoundNode? VisitPointerElementAccess(BoundPointerElementAccess node) { var result = base.VisitPointerElementAccess(node); var type = TypeWithAnnotations.Create(node.Type); SetLvalueResultType(node, type); return result; } public override BoundNode? VisitRefTypeOperator(BoundRefTypeOperator node) { VisitRvalue(node.Operand); SetNotNullResult(node); return null; } public override BoundNode? VisitMakeRefOperator(BoundMakeRefOperator node) { var result = base.VisitMakeRefOperator(node); SetNotNullResult(node); return result; } public override BoundNode? VisitRefValueOperator(BoundRefValueOperator node) { var result = base.VisitRefValueOperator(node); var type = TypeWithAnnotations.Create(node.Type, node.NullableAnnotation); SetLvalueResultType(node, type); return result; } private TypeWithState InferResultNullability(BoundUserDefinedConditionalLogicalOperator node) { if (node.OperatorKind.IsLifted()) { // https://github.com/dotnet/roslyn/issues/33879 Conversions: Lifted operator // Should this use the updated flow type and state? How should it compute nullability? return TypeWithState.Create(node.Type, NullableFlowState.NotNull); } // Update method based on inferred operand types: see https://github.com/dotnet/roslyn/issues/29605. // Analyze operator result properly (honoring [Maybe|NotNull] and [Maybe|NotNullWhen] attribute annotations) https://github.com/dotnet/roslyn/issues/32671 if ((object)node.LogicalOperator != null && node.LogicalOperator.ParameterCount == 2) { return GetReturnTypeWithState(node.LogicalOperator); } else { return default; } } protected override void AfterLeftChildOfBinaryLogicalOperatorHasBeenVisited(BoundExpression node, BoundExpression right, bool isAnd, bool isBool, ref LocalState leftTrue, ref LocalState leftFalse) { Debug.Assert(!IsConditionalState); TypeWithState leftType = ResultType; // https://github.com/dotnet/roslyn/issues/29605 Update operator methods based on inferred operand types. MethodSymbol? logicalOperator = null; MethodSymbol? trueFalseOperator = null; BoundExpression? left = null; switch (node.Kind) { case BoundKind.BinaryOperator: Debug.Assert(!((BoundBinaryOperator)node).OperatorKind.IsUserDefined()); break; case BoundKind.UserDefinedConditionalLogicalOperator: var binary = (BoundUserDefinedConditionalLogicalOperator)node; if (binary.LogicalOperator != null && binary.LogicalOperator.ParameterCount == 2) { logicalOperator = binary.LogicalOperator; left = binary.Left; trueFalseOperator = isAnd ? binary.FalseOperator : binary.TrueOperator; if ((object)trueFalseOperator != null && trueFalseOperator.ParameterCount != 1) { trueFalseOperator = null; } } break; default: throw ExceptionUtilities.UnexpectedValue(node.Kind); } Debug.Assert(trueFalseOperator is null || logicalOperator is object); Debug.Assert(logicalOperator is null || left is object); // Analyze operator call properly (honoring [Disallow|Allow|Maybe|NotNull] attribute annotations) https://github.com/dotnet/roslyn/issues/32671 if (trueFalseOperator is object) { ReportArgumentWarnings(left!, leftType, trueFalseOperator.Parameters[0]); } if (logicalOperator is object) { ReportArgumentWarnings(left!, leftType, logicalOperator.Parameters[0]); } Visit(right); TypeWithState rightType = ResultType; SetResultType(node, InferResultNullabilityOfBinaryLogicalOperator(node, leftType, rightType)); if (logicalOperator is object) { ReportArgumentWarnings(right, rightType, logicalOperator.Parameters[1]); } AfterRightChildOfBinaryLogicalOperatorHasBeenVisited(node, right, isAnd, isBool, ref leftTrue, ref leftFalse); } private TypeWithState InferResultNullabilityOfBinaryLogicalOperator(BoundExpression node, TypeWithState leftType, TypeWithState rightType) { return node switch { BoundBinaryOperator binary => InferResultNullability(binary.OperatorKind, binary.MethodOpt, binary.Type, leftType, rightType), BoundUserDefinedConditionalLogicalOperator userDefined => InferResultNullability(userDefined), _ => throw ExceptionUtilities.UnexpectedValue(node) }; } public override BoundNode? VisitAwaitExpression(BoundAwaitExpression node) { var result = base.VisitAwaitExpression(node); var awaitableInfo = node.AwaitableInfo; var placeholder = awaitableInfo.AwaitableInstancePlaceholder; Debug.Assert(placeholder is object); _awaitablePlaceholdersOpt ??= PooledDictionary.GetInstance(); _awaitablePlaceholdersOpt.Add(placeholder, (node.Expression, _visitResult)); Visit(awaitableInfo); _awaitablePlaceholdersOpt.Remove(placeholder); if (node.Type.IsValueType || node.HasErrors || awaitableInfo.GetResult is null) { SetNotNullResult(node); } else { // It is possible for the awaiter type returned from GetAwaiter to not be a named type. e.g. it could be a type parameter. // Proper handling of this is additional work which only benefits a very uncommon scenario, // so we will just use the originally bound GetResult method in this case. var getResult = awaitableInfo.GetResult; var reinferredGetResult = _visitResult.RValueType.Type is NamedTypeSymbol taskAwaiterType ? getResult.OriginalDefinition.AsMember(taskAwaiterType) : getResult; SetResultType(node, reinferredGetResult.ReturnTypeWithAnnotations.ToTypeWithState()); } return result; } public override BoundNode? VisitTypeOfOperator(BoundTypeOfOperator node) { var result = base.VisitTypeOfOperator(node); SetResultType(node, TypeWithState.Create(node.Type, NullableFlowState.NotNull)); return result; } public override BoundNode? VisitMethodInfo(BoundMethodInfo node) { var result = base.VisitMethodInfo(node); SetNotNullResult(node); return result; } public override BoundNode? VisitFieldInfo(BoundFieldInfo node) { var result = base.VisitFieldInfo(node); SetNotNullResult(node); return result; } public override BoundNode? VisitDefaultLiteral(BoundDefaultLiteral node) { // Can occur in error scenarios and lambda scenarios var result = base.VisitDefaultLiteral(node); SetResultType(node, TypeWithState.Create(node.Type, NullableFlowState.MaybeDefault)); return result; } public override BoundNode? VisitDefaultExpression(BoundDefaultExpression node) { Debug.Assert(!this.IsConditionalState); var result = base.VisitDefaultExpression(node); TypeSymbol type = node.Type; if (EmptyStructTypeCache.IsTrackableStructType(type)) { int slot = GetOrCreatePlaceholderSlot(node); if (slot > 0) { this.State[slot] = NullableFlowState.NotNull; InheritNullableStateOfTrackableStruct(type, slot, valueSlot: -1, isDefaultValue: true); } } // https://github.com/dotnet/roslyn/issues/33344: this fails to produce an updated tuple type for a default expression // (should produce nullable element types for those elements that are of reference types) SetResultType(node, TypeWithState.ForType(type)); return result; } public override BoundNode? VisitIsOperator(BoundIsOperator node) { Debug.Assert(!this.IsConditionalState); var operand = node.Operand; var typeExpr = node.TargetType; var result = base.VisitIsOperator(node); Debug.Assert(node.Type.SpecialType == SpecialType.System_Boolean); Split(); LearnFromNonNullTest(operand, ref StateWhenTrue); if (typeExpr.Type?.SpecialType == SpecialType.System_Object) { LearnFromNullTest(operand, ref StateWhenFalse); } VisitTypeExpression(typeExpr); SetNotNullResult(node); return result; } public override BoundNode? VisitAsOperator(BoundAsOperator node) { var argumentType = VisitRvalueWithState(node.Operand); NullableFlowState resultState = NullableFlowState.NotNull; var type = node.Type; if (type.CanContainNull()) { switch (node.Conversion.Kind) { case ConversionKind.Identity: case ConversionKind.ImplicitReference: case ConversionKind.Boxing: case ConversionKind.ImplicitNullable: resultState = argumentType.State; break; default: resultState = NullableFlowState.MaybeDefault; break; } } VisitTypeExpression(node.TargetType); SetResultType(node, TypeWithState.Create(type, resultState)); return null; } public override BoundNode? VisitSizeOfOperator(BoundSizeOfOperator node) { var result = base.VisitSizeOfOperator(node); VisitTypeExpression(node.SourceType); SetNotNullResult(node); return result; } public override BoundNode? VisitArgList(BoundArgList node) { var result = base.VisitArgList(node); Debug.Assert(node.Type.SpecialType == SpecialType.System_RuntimeArgumentHandle); SetNotNullResult(node); return result; } public override BoundNode? VisitArgListOperator(BoundArgListOperator node) { VisitArgumentsEvaluate(node.Syntax, node.Arguments, node.ArgumentRefKindsOpt, parametersOpt: default, argsToParamsOpt: default, expanded: false); Debug.Assert(node.Type is null); SetNotNullResult(node); return null; } public override BoundNode? VisitLiteral(BoundLiteral node) { var result = base.VisitLiteral(node); Debug.Assert(!IsConditionalState); SetResultType(node, TypeWithState.Create(node.Type, node.Type?.CanContainNull() != false && node.ConstantValue?.IsNull == true ? NullableFlowState.MaybeDefault : NullableFlowState.NotNull)); if (node.ConstantValue?.IsBoolean == true) { Split(); if (node.ConstantValue.BooleanValue) { StateWhenFalse = UnreachableState(); } else { StateWhenTrue = UnreachableState(); } } return result; } public override BoundNode? VisitPreviousSubmissionReference(BoundPreviousSubmissionReference node) { var result = base.VisitPreviousSubmissionReference(node); Debug.Assert(node.WasCompilerGenerated); SetNotNullResult(node); return result; } public override BoundNode? VisitHostObjectMemberReference(BoundHostObjectMemberReference node) { var result = base.VisitHostObjectMemberReference(node); Debug.Assert(node.WasCompilerGenerated); SetNotNullResult(node); return result; } public override BoundNode? VisitPseudoVariable(BoundPseudoVariable node) { var result = base.VisitPseudoVariable(node); SetNotNullResult(node); return result; } public override BoundNode? VisitRangeExpression(BoundRangeExpression node) { var result = base.VisitRangeExpression(node); SetNotNullResult(node); return result; } public override BoundNode? VisitRangeVariable(BoundRangeVariable node) { VisitWithoutDiagnostics(node.Value); SetNotNullResult(node); // https://github.com/dotnet/roslyn/issues/29863 Need to review this return null; } public override BoundNode? VisitLabel(BoundLabel node) { var result = base.VisitLabel(node); SetUnknownResultNullability(node); return result; } public override BoundNode? VisitDynamicMemberAccess(BoundDynamicMemberAccess node) { var receiver = node.Receiver; VisitRvalue(receiver); _ = CheckPossibleNullReceiver(receiver); Debug.Assert(node.Type.IsDynamic()); var result = TypeWithAnnotations.Create(node.Type); SetLvalueResultType(node, result); return null; } public override BoundNode? VisitDynamicInvocation(BoundDynamicInvocation node) { var expr = node.Expression; VisitRvalue(expr); // If the expression was a MethodGroup, check nullability of receiver. var receiverOpt = (expr as BoundMethodGroup)?.ReceiverOpt; if (TryGetMethodGroupReceiverNullability(receiverOpt, out TypeWithState receiverType)) { CheckPossibleNullReceiver(receiverOpt, receiverType, checkNullableValueType: false); } VisitArgumentsEvaluate(node.Syntax, node.Arguments, node.ArgumentRefKindsOpt, parametersOpt: default, argsToParamsOpt: default, expanded: false); Debug.Assert(node.Type.IsDynamic()); Debug.Assert(node.Type.IsReferenceType); var result = TypeWithAnnotations.Create(node.Type, NullableAnnotation.Oblivious); SetLvalueResultType(node, result); return null; } public override BoundNode? VisitEventAssignmentOperator(BoundEventAssignmentOperator node) { var receiverOpt = node.ReceiverOpt; VisitRvalue(receiverOpt); Debug.Assert(!IsConditionalState); var @event = node.Event; if (!@event.IsStatic) { @event = (EventSymbol)AsMemberOfType(ResultType.Type, @event); // https://github.com/dotnet/roslyn/issues/30598: Mark receiver as not null // after arguments have been visited, and only if the receiver has not changed. _ = CheckPossibleNullReceiver(receiverOpt); SetUpdatedSymbol(node, node.Event, @event); } VisitRvalue(node.Argument); // https://github.com/dotnet/roslyn/issues/31018: Check for delegate mismatch. if (node.Argument.ConstantValue?.IsNull != true && MakeMemberSlot(receiverOpt, @event) is > 0 and var memberSlot) { this.State[memberSlot] = node.IsAddition ? this.State[memberSlot].Meet(ResultType.State) : NullableFlowState.MaybeNull; } SetNotNullResult(node); // https://github.com/dotnet/roslyn/issues/29969 Review whether this is the correct result return null; } public override BoundNode? VisitDynamicObjectCreationExpression(BoundDynamicObjectCreationExpression node) { Debug.Assert(!IsConditionalState); var arguments = node.Arguments; var (argumentResults, _, _, _) = VisitArgumentsEvaluate(node.Syntax, arguments, node.ArgumentRefKindsOpt, parametersOpt: default, argsToParamsOpt: default, expanded: false); VisitObjectOrDynamicObjectCreation(node, arguments, argumentResults, node.InitializerExpressionOpt); return null; } public override BoundNode? VisitObjectInitializerExpression(BoundObjectInitializerExpression node) { // Only reachable from bad expression. Otherwise handled in VisitObjectCreationExpression(). // https://github.com/dotnet/roslyn/issues/35042: Do we need to analyze child expressions anyway for the public API? SetNotNullResult(node); return null; } public override BoundNode? VisitCollectionInitializerExpression(BoundCollectionInitializerExpression node) { // Only reachable from bad expression. Otherwise handled in VisitObjectCreationExpression(). // https://github.com/dotnet/roslyn/issues/35042: Do we need to analyze child expressions anyway for the public API? SetNotNullResult(node); return null; } public override BoundNode? VisitDynamicCollectionElementInitializer(BoundDynamicCollectionElementInitializer node) { // Only reachable from bad expression. Otherwise handled in VisitObjectCreationExpression(). // https://github.com/dotnet/roslyn/issues/35042: Do we need to analyze child expressions anyway for the public API? SetNotNullResult(node); return null; } public override BoundNode? VisitImplicitReceiver(BoundImplicitReceiver node) { var result = base.VisitImplicitReceiver(node); SetNotNullResult(node); return result; } public override BoundNode? VisitAnonymousPropertyDeclaration(BoundAnonymousPropertyDeclaration node) { throw ExceptionUtilities.Unreachable; } public override BoundNode? VisitNoPiaObjectCreationExpression(BoundNoPiaObjectCreationExpression node) { var result = base.VisitNoPiaObjectCreationExpression(node); SetResultType(node, TypeWithState.Create(node.Type, NullableFlowState.NotNull)); return result; } public override BoundNode? VisitNewT(BoundNewT node) { VisitObjectOrDynamicObjectCreation(node, ImmutableArray.Empty, ImmutableArray.Empty, node.InitializerExpressionOpt); return null; } public override BoundNode? VisitArrayInitialization(BoundArrayInitialization node) { var result = base.VisitArrayInitialization(node); SetNotNullResult(node); return result; } private void SetUnknownResultNullability(BoundExpression expression) { SetResultType(expression, TypeWithState.Create(expression.Type, default)); } public override BoundNode? VisitStackAllocArrayCreation(BoundStackAllocArrayCreation node) { var result = base.VisitStackAllocArrayCreation(node); Debug.Assert(node.Type is null || node.Type.IsErrorType() || node.Type.IsRefLikeType); SetNotNullResult(node); return result; } public override BoundNode? VisitDynamicIndexerAccess(BoundDynamicIndexerAccess node) { var receiver = node.Receiver; VisitRvalue(receiver); // 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(receiver); VisitArgumentsEvaluate(node.Syntax, node.Arguments, node.ArgumentRefKindsOpt, parametersOpt: default, argsToParamsOpt: default, expanded: false); Debug.Assert(node.Type.IsDynamic()); var result = TypeWithAnnotations.Create(node.Type, NullableAnnotation.Oblivious); SetLvalueResultType(node, result); return null; } private bool CheckPossibleNullReceiver(BoundExpression? receiverOpt, bool checkNullableValueType = false) { return CheckPossibleNullReceiver(receiverOpt, ResultType, checkNullableValueType); } private bool CheckPossibleNullReceiver(BoundExpression? receiverOpt, TypeWithState resultType, bool checkNullableValueType) { Debug.Assert(!this.IsConditionalState); bool reportedDiagnostic = false; if (receiverOpt != null && this.State.Reachable) { var resultTypeSymbol = resultType.Type; if (resultTypeSymbol is null) { return false; } #if DEBUG Debug.Assert(receiverOpt.Type is null || AreCloseEnough(receiverOpt.Type, resultTypeSymbol)); #endif if (!ReportPossibleNullReceiverIfNeeded(resultTypeSymbol, resultType.State, checkNullableValueType, receiverOpt.Syntax, out reportedDiagnostic)) { return reportedDiagnostic; } LearnFromNonNullTest(receiverOpt, ref this.State); } return reportedDiagnostic; } // Returns false if the type wasn't interesting private bool ReportPossibleNullReceiverIfNeeded(TypeSymbol type, NullableFlowState state, bool checkNullableValueType, SyntaxNode syntax, out bool reportedDiagnostic) { reportedDiagnostic = false; if (state.MayBeNull()) { bool isValueType = type.IsValueType; if (isValueType && (!checkNullableValueType || !type.IsNullableTypeOrTypeParameter() || type.GetNullableUnderlyingType().IsErrorType())) { return false; } ReportDiagnostic(isValueType ? ErrorCode.WRN_NullableValueTypeMayBeNull : ErrorCode.WRN_NullReferenceReceiver, syntax); reportedDiagnostic = true; } return true; } private void CheckExtensionMethodThisNullability(BoundExpression expr, Conversion conversion, ParameterSymbol parameter, TypeWithState result) { VisitArgumentConversionAndInboundAssignmentsAndPreConditions( conversionOpt: null, expr, conversion, parameter.RefKind, parameter, parameter.TypeWithAnnotations, GetParameterAnnotations(parameter), new VisitArgumentResult(new VisitResult(result, result.ToTypeWithAnnotations(compilation)), stateForLambda: default), extensionMethodThisArgument: true); } private static bool IsNullabilityMismatch(TypeWithAnnotations type1, TypeWithAnnotations type2) { // Note, when we are paying attention to nullability, we ignore oblivious mismatch. // See TypeCompareKind.ObliviousNullableModifierMatchesAny return type1.Equals(type2, TypeCompareKind.AllIgnoreOptions) && !type1.Equals(type2, TypeCompareKind.AllIgnoreOptions & ~TypeCompareKind.IgnoreNullableModifiersForReferenceTypes); } private static bool IsNullabilityMismatch(TypeSymbol type1, TypeSymbol type2) { // Note, when we are paying attention to nullability, we ignore oblivious mismatch. // See TypeCompareKind.ObliviousNullableModifierMatchesAny return type1.Equals(type2, TypeCompareKind.AllIgnoreOptions) && !type1.Equals(type2, TypeCompareKind.AllIgnoreOptions & ~TypeCompareKind.IgnoreNullableModifiersForReferenceTypes); } public override BoundNode? VisitQueryClause(BoundQueryClause node) { var result = base.VisitQueryClause(node); SetNotNullResult(node); // https://github.com/dotnet/roslyn/issues/29863 Implement nullability analysis in LINQ queries return result; } public override BoundNode? VisitNameOfOperator(BoundNameOfOperator node) { var result = base.VisitNameOfOperator(node); SetResultType(node, TypeWithState.Create(node.Type, NullableFlowState.NotNull)); return result; } public override BoundNode? VisitNamespaceExpression(BoundNamespaceExpression node) { var result = base.VisitNamespaceExpression(node); SetUnknownResultNullability(node); return result; } public override BoundNode? VisitInterpolatedString(BoundInterpolatedString node) { var result = base.VisitInterpolatedString(node); SetResultType(node, TypeWithState.Create(node.Type, NullableFlowState.NotNull)); return result; } public override BoundNode? VisitStringInsert(BoundStringInsert node) { var result = base.VisitStringInsert(node); SetUnknownResultNullability(node); return result; } public override BoundNode? VisitConvertedStackAllocExpression(BoundConvertedStackAllocExpression node) { var result = base.VisitConvertedStackAllocExpression(node); SetNotNullResult(node); return result; } public override BoundNode? VisitDiscardExpression(BoundDiscardExpression node) { var result = TypeWithAnnotations.Create(node.Type); var rValueType = TypeWithState.ForType(node.Type); SetResult(node, rValueType, result); return null; } public override BoundNode? VisitThrowExpression(BoundThrowExpression node) { VisitThrow(node.Expression); SetResultType(node, default); return null; } public override BoundNode? VisitThrowStatement(BoundThrowStatement node) { VisitThrow(node.ExpressionOpt); return null; } private void VisitThrow(BoundExpression? expr) { if (expr != null) { var result = VisitRvalueWithState(expr); // Cases: // null // null! // Other (typed) expression, including suppressed ones if (result.MayBeNull) { ReportDiagnostic(ErrorCode.WRN_ThrowPossibleNull, expr.Syntax); } } SetUnreachable(); } public override BoundNode? VisitYieldReturnStatement(BoundYieldReturnStatement node) { BoundExpression expr = node.Expression; if (expr == null) { return null; } var method = (MethodSymbol)CurrentSymbol; TypeWithAnnotations elementType = InMethodBinder.GetIteratorElementTypeFromReturnType(compilation, RefKind.None, method.ReturnType, errorLocation: null, diagnostics: null); _ = VisitOptionalImplicitConversion(expr, elementType, useLegacyWarnings: false, trackMembers: false, AssignmentKind.Return); return null; } protected override void VisitCatchBlock(BoundCatchBlock node, ref LocalState finallyState) { TakeIncrementalSnapshot(node); if (node.Locals.Length > 0) { LocalSymbol local = node.Locals[0]; if (local.DeclarationKind == LocalDeclarationKind.CatchVariable) { int slot = GetOrCreateSlot(local); if (slot > 0) this.State[slot] = NullableFlowState.NotNull; } } if (node.ExceptionSourceOpt != null) { VisitWithoutDiagnostics(node.ExceptionSourceOpt); } base.VisitCatchBlock(node, ref finallyState); } public override BoundNode? VisitLockStatement(BoundLockStatement node) { VisitRvalue(node.Argument); _ = CheckPossibleNullReceiver(node.Argument); VisitStatement(node.Body); return null; } public override BoundNode? VisitAttribute(BoundAttribute node) { VisitArguments(node, node.ConstructorArguments, ImmutableArray.Empty, node.Constructor, argsToParamsOpt: node.ConstructorArgumentsToParamsOpt, expanded: node.ConstructorExpanded, invokedAsExtensionMethod: false); foreach (var assignment in node.NamedArguments) { Visit(assignment); } SetNotNullResult(node); return null; } public override BoundNode? VisitExpressionWithNullability(BoundExpressionWithNullability node) { var typeWithAnnotations = TypeWithAnnotations.Create(node.Type, node.NullableAnnotation); SetResult(node.Expression, typeWithAnnotations.ToTypeWithState(), typeWithAnnotations); return null; } public override BoundNode? VisitDeconstructValuePlaceholder(BoundDeconstructValuePlaceholder node) { SetNotNullResult(node); return null; } public override BoundNode? VisitObjectOrCollectionValuePlaceholder(BoundObjectOrCollectionValuePlaceholder node) { SetNotNullResult(node); return null; } public override BoundNode? VisitAwaitableValuePlaceholder(BoundAwaitableValuePlaceholder node) { if (_awaitablePlaceholdersOpt != null && _awaitablePlaceholdersOpt.TryGetValue(node, out var value)) { var result = value.Result; SetResult(node, result.RValueType, result.LValueType); } else { SetNotNullResult(node); } return null; } public override BoundNode? VisitAwaitableInfo(BoundAwaitableInfo node) { Visit(node.AwaitableInstancePlaceholder); Visit(node.GetAwaiter); return null; } public override BoundNode? VisitFunctionPointerInvocation(BoundFunctionPointerInvocation node) { _ = Visit(node.InvokedExpression); Debug.Assert(ResultType is TypeWithState { Type: FunctionPointerTypeSymbol { }, State: NullableFlowState.NotNull }); _ = VisitArguments( node, node.Arguments, node.ArgumentRefKindsOpt, node.FunctionPointer.Signature, argsToParamsOpt: default, expanded: false, invokedAsExtensionMethod: false); var returnTypeWithAnnotations = node.FunctionPointer.Signature.ReturnTypeWithAnnotations; SetResult(node, returnTypeWithAnnotations.ToTypeWithState(), returnTypeWithAnnotations); return null; } protected override string Dump(LocalState state) { if (!state.Reachable) return "unreachable"; var pooledBuilder = PooledStringBuilder.GetInstance(); var builder = pooledBuilder.Builder; for (int i = 0; i < state.Capacity; i++) { if (nameForSlot(i) is string name) { builder.Append(name); var annotation = state[i] switch { NullableFlowState.MaybeNull => "?", NullableFlowState.MaybeDefault => "??", _ => "!" }; builder.Append(annotation); } } return pooledBuilder.ToStringAndFree(); string? nameForSlot(int slot) { if (slot < 0) return null; VariableIdentifier id = this.variableBySlot[slot]; var name = id.Symbol?.Name; if (name == null) return null; return nameForSlot(id.ContainingSlot) is string containingSlotName ? containingSlotName + "." + name : name; } } protected override bool Meet(ref LocalState self, ref LocalState other) { if (!self.Reachable) return false; if (!other.Reachable) { self = other.Clone(); return true; } if (self.Capacity != other.Capacity) { Normalize(ref self); Normalize(ref other); } return self.Meet(in other); } protected override bool Join(ref LocalState self, ref LocalState other) { if (!other.Reachable) return false; if (!self.Reachable) { self = other.Clone(); return true; } if (self.Capacity != other.Capacity) { Normalize(ref self); Normalize(ref other); } return self.Join(in other); } [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] #if REFERENCE_STATE internal class LocalState : ILocalDataFlowState #else internal struct LocalState : ILocalDataFlowState #endif { // The representation of a state is a bit vector with two bits per slot: // (false, false) => NotNull, (false, true) => MaybeNull, (true, true) => MaybeDefault. // Slot 0 is used to represent whether the state is reachable (true) or not. private BitVector _state; private LocalState(BitVector state) => this._state = state; public bool Reachable => _state[0]; public bool NormalizeToBottom => false; public static LocalState ReachableState(int capacity) { if (capacity < 1) capacity = 1; BitVector state = BitVector.Create(capacity * 2); state[0] = true; return new LocalState(state); } public static LocalState UnreachableState { get { BitVector state = BitVector.Create(2); state[0] = false; return new LocalState(state); } } public int Capacity => _state.Capacity / 2; public void EnsureCapacity(int capacity) => _state.EnsureCapacity(capacity * 2); public NullableFlowState this[int slot] { get { if (slot < Capacity && this.Reachable) { slot *= 2; return (_state[slot + 1], _state[slot]) switch { (false, false) => NullableFlowState.NotNull, (false, true) => NullableFlowState.MaybeNull, (true, false) => throw ExceptionUtilities.UnexpectedValue(slot), (true, true) => NullableFlowState.MaybeDefault }; } return NullableFlowState.NotNull; } set { // No states should be modified in unreachable code, as there is only one unreachable state. if (!this.Reachable) return; slot *= 2; _state[slot] = (value != NullableFlowState.NotNull); _state[slot + 1] = (value == NullableFlowState.MaybeDefault); } } /// /// Produce a duplicate of this flow analysis state. /// /// public LocalState Clone() => new LocalState(_state.Clone()); public bool Join(in LocalState other) => _state.UnionWith(in other._state); public bool Meet(in LocalState other) => _state.IntersectWith(in other._state); internal string GetDebuggerDisplay() { var pooledBuilder = PooledStringBuilder.GetInstance(); var builder = pooledBuilder.Builder; builder.Append(" "); int n = Math.Min(Capacity, 8); for (int i = n - 1; i >= 0; i--) builder.Append(_state[i * 2] ? '?' : '!'); return pooledBuilder.ToStringAndFree(); } } internal sealed class LocalFunctionState : AbstractLocalFunctionState { /// /// Defines the starting state used in the local function body to /// produce diagnostics and determine types. /// public LocalState StartingState; public LocalFunctionState(LocalState unreachableState) : base(unreachableState.Clone(), unreachableState.Clone()) { StartingState = unreachableState; } } protected override LocalFunctionState CreateLocalFunctionState() => new LocalFunctionState(UnreachableState()); private sealed class NullabilityInfoTypeComparer : IEqualityComparer<(NullabilityInfo info, TypeSymbol? type)> { public static readonly NullabilityInfoTypeComparer Instance = new NullabilityInfoTypeComparer(); public bool Equals((NullabilityInfo info, TypeSymbol? type) x, (NullabilityInfo info, TypeSymbol? type) y) { return x.info.Equals(y.info) && Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(x.type, y.type); } public int GetHashCode((NullabilityInfo info, TypeSymbol? type) obj) { return obj.GetHashCode(); } } private sealed class ExpressionAndSymbolEqualityComparer : IEqualityComparer<(BoundNode? expr, Symbol symbol)> { internal static readonly ExpressionAndSymbolEqualityComparer Instance = new ExpressionAndSymbolEqualityComparer(); private ExpressionAndSymbolEqualityComparer() { } public bool Equals((BoundNode? expr, Symbol symbol) x, (BoundNode? expr, Symbol symbol) y) { RoslynDebug.Assert(x.symbol is object); RoslynDebug.Assert(y.symbol is object); // We specifically use reference equality for the symbols here because the BoundNode should be immutable. // We should be storing and retrieving the exact same instance of the symbol, not just an "equivalent" // symbol. return x.expr == y.expr && (object)x.symbol == y.symbol; } public int GetHashCode((BoundNode? expr, Symbol symbol) obj) { RoslynDebug.Assert(obj.symbol is object); return Hash.Combine(obj.expr, obj.symbol.GetHashCode()); } } } }