File: NullableHelpers.cs
Web Access
Project: ..\..\..\src\CodeStyle\CSharp\CodeFixes\Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis
{
    internal static class NullableHelpers
    {
        /// <summary>
        /// Gets the declared symbol and root operation from the passed in declarationSyntax and calls
        /// <see cref="IsSymbolAssignedPossiblyNullValue(SemanticModel, IOperation, ISymbol)"/>. Note
        /// that this is bool and not bool? because we know that the symbol is at the very least declared, 
        /// so there's no need to return a null value. 
        /// </summary>
        public static bool IsDeclaredSymbolAssignedPossiblyNullValue(SemanticModel semanticModel, SyntaxNode declarationSyntax, CancellationToken cancellationToken)
        {
            var declaredSymbol = semanticModel.GetRequiredDeclaredSymbol(declarationSyntax, cancellationToken);
            var declaredOperation = semanticModel.GetRequiredOperation(declarationSyntax, cancellationToken);
 
            var rootOperation = declaredOperation;
 
            // Walk up the tree to find a root for the operation
            // that contains the declaration
            while (rootOperation is not IBlockOperation &&
                rootOperation.Parent is not null)
            {
                rootOperation = rootOperation.Parent;
            }
 
            return IsSymbolAssignedPossiblyNullValue(semanticModel, rootOperation, declaredSymbol) == true;
        }
 
        /// <summary>
        /// Given an operation, goes through all decendent operations and returns true if the symbol passed in
        /// is ever assigned a possibly null value as determined by nullable flow state. Returns
        /// null if no references are found, letting the caller determine what to do with that information
        /// </summary>
        public static bool? IsSymbolAssignedPossiblyNullValue(SemanticModel semanticModel, IOperation operation, ISymbol symbol)
        {
            var references = operation.DescendantsAndSelf()
                .Where(o => IsSymbolReferencedByOperation(o, symbol));
 
            var hasReference = false;
 
            foreach (var reference in references)
            {
                hasReference = true;
 
                // foreach statements are handled special because the iterator is not assignable, so the elementtype 
                // annotation is accurate for determining if the loop declaration has a reference that allows the symbol
                // to be null
                if (reference is IForEachLoopOperation forEachLoop)
                {
                    var foreachInfo = semanticModel.GetForEachStatementInfo((CommonForEachStatementSyntax)forEachLoop.Syntax);
 
                    if (foreachInfo.ElementType is null)
                    {
                        continue;
                    }
 
                    // Use NotAnnotated here to keep both Annotated and None (oblivious) treated the same, since
                    // this is directly looking at the annotation and not the flow state
                    if (foreachInfo.ElementType.NullableAnnotation != NullableAnnotation.NotAnnotated)
                    {
                        return true;
                    }
 
                    continue;
                }
 
                var syntax = reference is IVariableDeclaratorOperation variableDeclarator
                    ? variableDeclarator.GetVariableInitializer()!.Value.Syntax
                    : reference.Syntax;
 
                var typeInfo = semanticModel.GetTypeInfo(syntax);
 
                if (typeInfo.Nullability.FlowState == NullableFlowState.MaybeNull)
                {
                    return true;
                }
            }
 
            return hasReference ? (bool?)false : null;
        }
 
        /// <summary>
        /// Determines if an operations references a specific symbol. Note that this will recurse in some
        /// cases to work for operations like IAssignmentOperation, which logically references a symbol even if it
        /// is the Target operation that actually does. 
        /// </summary>
        private static bool IsSymbolReferencedByOperation(IOperation operation, ISymbol symbol)
            => operation switch
            {
                ILocalReferenceOperation localReference => localReference.Local.Equals(symbol),
                IParameterReferenceOperation parameterReference => parameterReference.Parameter.Equals(symbol),
                IAssignmentOperation assignment => IsSymbolReferencedByOperation(assignment.Target, symbol),
                ITupleOperation tupleOperation => tupleOperation.Elements.Any(static (element, symbol) => IsSymbolReferencedByOperation(element, symbol), symbol),
                IForEachLoopOperation { LoopControlVariable: IVariableDeclaratorOperation variableDeclarator } => variableDeclarator.Symbol.Equals(symbol),
 
                // A variable initializer is required for this to be a meaningful operation for determining possible null assignment
                IVariableDeclaratorOperation variableDeclarator => variableDeclarator.GetVariableInitializer() != null && variableDeclarator.Symbol.Equals(symbol),
                _ => false
            };
    }
}