File: src\roslyn\src\Analyzers\CSharp\Analyzers\UsePatternMatching\CSharpAsAndNullCheckDiagnosticAnalyzer.Analyzer.cs
Web Access
Project: Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// 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.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.UsePatternMatching;

internal sealed partial class CSharpAsAndNullCheckDiagnosticAnalyzer
{
    private readonly struct Analyzer
    {
        private readonly SemanticModel _semanticModel;
        private readonly ILocalSymbol _localSymbol;
        private readonly ExpressionSyntax _comparison;
        private readonly ExpressionSyntax _operand;
        private readonly LocalDeclarationStatementSyntax _localStatement;
        private readonly VariableDeclaratorSyntax _declarator;
        private readonly SyntaxNode _enclosingBlock;
        private readonly CancellationToken _cancellationToken;

        private Analyzer(
            SemanticModel semanticModel,
            ILocalSymbol localSymbol,
            ExpressionSyntax comparison,
            ExpressionSyntax operand,
            LocalDeclarationStatementSyntax localStatement,
            VariableDeclaratorSyntax declarator,
            SyntaxNode enclosingBlock,
            CancellationToken cancellationToken)
        {
            Contract.ThrowIfNull(semanticModel);
            Contract.ThrowIfNull(localSymbol);
            Contract.ThrowIfNull(comparison);
            Contract.ThrowIfNull(operand);
            Debug.Assert(localStatement.IsKind(SyntaxKind.LocalDeclarationStatement));
            Debug.Assert(enclosingBlock.Kind() is SyntaxKind.Block or SyntaxKind.SwitchSection);

            _semanticModel = semanticModel;
            _comparison = comparison;
            _localSymbol = localSymbol;
            _operand = operand;
            _localStatement = localStatement;
            _declarator = declarator;
            _enclosingBlock = enclosingBlock;
            _cancellationToken = cancellationToken;
        }

        public static bool CanSafelyConvertToPatternMatching(
            SemanticModel semanticModel,
            ILocalSymbol localSymbol,
            ExpressionSyntax comparison,
            ExpressionSyntax operand,
            LocalDeclarationStatementSyntax localStatement,
            VariableDeclaratorSyntax declarator,
            SyntaxNode enclosingBlock,
            CancellationToken cancellationToken)
        {
            var analyzer = new Analyzer(semanticModel, localSymbol, comparison, operand, localStatement, declarator, enclosingBlock, cancellationToken);
            return analyzer.CanSafelyConvertToPatternMatching();
        }

        private bool CanSafelyConvertToPatternMatching()
        {
            return CheckDefiniteAssignment() && CheckNoInterveningMutations();
        }

        // To convert a null-check to pattern-matching, we should make sure of a few things:
        //
        //      (1) The pattern variable may not be used before the point of declaration.
        //
        //          {
        //              var use = t;
        //              if (x is T t) {}
        //          }
        //
        //      (2) The pattern variable may not be used outside of the new scope which
        //          is determined by the parent statement.
        //
        //          {
        //              if (x is T t) {}
        //          }
        //
        //          var use = t;
        //
        //      (3) The pattern variable may not be used before assignment in opposite
        //          branches, if any.
        //
        //          {
        //              if (x is T t) {}
        //              var use = t;
        //          }
        //
        // We walk up the tree from the point of null-check and see if any of the above is violated.
        private bool CheckDefiniteAssignment()
        {
            // Keep track of whether the pattern variable is definitely assigned when false/true.
            // We start by the null-check itself, if it's compared with '==', the pattern variable
            // will be definitely assigned when false, because we wrap the is-operator in a !-operator.
            var defAssignedWhenTrue = _comparison.Kind() is SyntaxKind.NotEqualsExpression or SyntaxKind.IsExpression;

            foreach (var current in _comparison.Ancestors())
            {
                // Checking for any conditional statement or expression that could possibly
                // affect or determine the state of definite-assignment of the pattern variable.
                switch (current.Kind())
                {
                    case SyntaxKind.LogicalAndExpression when !defAssignedWhenTrue:
                    case SyntaxKind.LogicalOrExpression when defAssignedWhenTrue:
                        // Since the pattern variable is only definitely assigned if the pattern
                        // succeeded, in the following cases it would not be safe to use pattern-matching.
                        // For example:
                        //
                        //      if ((x = o as string) == null && SomeExpression)
                        //      if ((x = o as string) != null || SomeExpression)
                        //
                        // Here, x would never be definitely assigned if pattern-matching were used.
                        return false;

                    case SyntaxKind.LogicalAndExpression:
                    case SyntaxKind.LogicalOrExpression:

                    // Parentheses and cast expressions do not contribute to the flow analysis.
                    case SyntaxKind.ParenthesizedExpression:
                    case SyntaxKind.CastExpression:

                    // Skip over declaration parts to get to the parenting statement
                    // which might be a for-statement or a local declaration statement.
                    case SyntaxKind.EqualsValueClause:
                    case SyntaxKind.VariableDeclarator:
                    case SyntaxKind.VariableDeclaration:
                        continue;

                    case SyntaxKind.LogicalNotExpression:
                        // The !-operator negates the definitive assignment state.
                        defAssignedWhenTrue = !defAssignedWhenTrue;
                        continue;

                    case SyntaxKind.ConditionalExpression:
                        var conditionalExpression = (ConditionalExpressionSyntax)current;
                        if (LocalFlowsIn(defAssignedWhenTrue
                                ? conditionalExpression.WhenFalse
                                : conditionalExpression.WhenTrue))
                        {
                            // In a conditional expression, the pattern variable
                            // would not be definitely assigned in the opposite branch.
                            return false;
                        }

                        return CheckExpression(conditionalExpression);

                    case SyntaxKind.ForStatement:
                        var forStatement = (ForStatementSyntax)current;
                        if (forStatement.Condition is null || !forStatement.Condition.Span.Contains(_comparison.Span))
                        {
                            // In a for-statement, only the condition expression
                            // can make this definitely assigned in the loop body.
                            return false;
                        }

                        return CheckLoop(forStatement, forStatement.Statement, defAssignedWhenTrue);

                    case SyntaxKind.WhileStatement:
                        var whileStatement = (WhileStatementSyntax)current;
                        return CheckLoop(whileStatement, whileStatement.Statement, defAssignedWhenTrue);

                    case SyntaxKind.IfStatement:
                        var ifStatement = (IfStatementSyntax)current;
                        var oppositeStatement = defAssignedWhenTrue
                            ? ifStatement.Else?.Statement
                            : ifStatement.Statement;

                        if (oppositeStatement != null)
                        {
                            var dataFlow = _semanticModel.AnalyzeRequiredDataFlow(oppositeStatement);
                            if (dataFlow.DataFlowsIn.Contains(_localSymbol))
                            {
                                // Access before assignment is not safe in the opposite branch
                                // as the variable is not definitely assigned at this point.
                                // For example:
                                //
                                //    if (o is string x) { }
                                //    else { Use(x); }
                                //
                                return false;
                            }

                            if (dataFlow.AlwaysAssigned.Contains(_localSymbol))
                            {
                                // If the variable is always assigned here, we don't need to check
                                // subsequent statements as it's definitely assigned afterwards.
                                // For example:
                                //
                                //     if (o is string x) { }
                                //     else { x = null; }
                                //
                                return true;
                            }
                        }

                        if (!defAssignedWhenTrue &&
                            !_semanticModel.AnalyzeRequiredControlFlow(ifStatement.Statement).EndPointIsReachable)
                        {
                            // Access before assignment here is only valid if we have a negative
                            // pattern-matching in an if-statement with an unreachable endpoint.
                            // For example:
                            //
                            //      if (!(o is string x)) {
                            //        return;
                            //      }
                            //
                            //      // The 'return' statement above ensures x is definitely assigned here
                            //      Console.WriteLine(x);
                            //
                            return true;
                        }

                        return CheckStatement(ifStatement);
                }

                switch (current)
                {
                    case ExpressionSyntax expression:
                        // If we reached here, it means we have a sub-expression that
                        // does not guarantee definite assignment. We should make sure that
                        // the pattern variable is not used outside of the expression boundaries.
                        return CheckExpression(expression);

                    case StatementSyntax statement:
                        // If we reached here, it means that the null-check is appeared in
                        // a statement. In that case, the variable would be actually in the
                        // scope in subsequent statements, but not definitely assigned.
                        // Therefore, we should ensure that there is no use before assignment.
                        return CheckStatement(statement);
                }

                // Bail out for error cases and unhandled cases.
                break;
            }

            return false;
        }

        private bool CheckNoInterveningMutations()
        {
            var semanticModel = _semanticModel;
            foreach (var node in _enclosingBlock.DescendantNodesAndSelf())
            {
                // Skip until we're after the initial `var v = x as y; statement.
                if (node.SpanStart < _localStatement.Span.End)
                    continue;

                // analyze up to the `x != null` check.
                if (node.Span.End >= _comparison.SpanStart)
                    break;

                if (node is ExpressionSyntax currentExpression &&
                    currentExpression.IsWrittenTo(_semanticModel, _cancellationToken))
                {
                    // Ok, we have a write of some value in between.  See if the initial statement used that value as
                    // well.  For example, if we have `var v = x[current] as y;` then if we see a `current++` in between
                    // us and the null check, then we don't want to move things as that could change semantics.
                    if (_declarator.DescendantNodesAndSelf().OfType<ExpressionSyntax>().Any(
                            e => SemanticEquivalence.AreEquivalent(semanticModel, e, currentExpression)))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private bool CheckLoop(SyntaxNode statement, StatementSyntax body, bool defAssignedWhenTrue)
        {
            if (_operand.Kind() == SyntaxKind.IdentifierName)
            {
                // We have something like:
                //
                //      var x = e as T;
                //      while (b != null) { ... }
                //
                // It's not necessarily safe to convert this to:
                //
                //      while (x is T b) { ... }
                //
                // That's because in this case, unlike the original code, we're
                // type-checking in every iteration, so we do not replace a
                // simple null check with the "is" operator if it's in a loop.
                return false;
            }

            if (!defAssignedWhenTrue && LocalFlowsIn(body))
            {
                // If the local is accessed before assignment
                // in the loop body, we should make sure that
                // the variable is definitely assigned by then.
                return false;
            }

            // The scope of the pattern variables for loops
            // does not leak out of the loop statement.
            return !IsAccessedOutOfScope(scope: statement);
        }

        private bool CheckExpression(ExpressionSyntax exprsesion)
        {
            // It wouldn't be safe to read after the pattern variable is
            // declared inside a sub-expression, because it would not be
            // definitely assigned after this point. It's possible to allow
            // use after assignment but it's rather unlikely to happen.
            return !IsAccessedOutOfScope(scope: exprsesion);
        }

        private bool CheckStatement(StatementSyntax statement)
        {
            Contract.ThrowIfNull(statement);

            // This is either an embedded statement or parented by a block.
            // If we're parented by a block, then that block will be the scope
            // of the new variable. Otherwise the scope is the statement itself.
            if (statement.Parent is BlockSyntax block)
            {
                // Check if the local is accessed before assignment 
                // in the subsequent statements. If so, this can't
                // be converted to pattern-matching.
                if (LocalFlowsIn(firstStatement: statement.GetNextStatement(),
                                 lastStatement: block.Statements.Last()))
                {
                    return false;
                }

                return !IsAccessedOutOfScope(scope: block);
            }
            else
            {
                return !IsAccessedOutOfScope(scope: statement);
            }
        }

        private bool IsAccessedOutOfScope(SyntaxNode scope)
        {
            Contract.ThrowIfNull(scope);

            var localStatementStart = _localStatement.SpanStart;
            var comparisonSpanStart = _comparison.SpanStart;
            var variableName = _localSymbol.Name;
            var scopeSpan = scope.Span;

            // Iterate over all descendant nodes to find possible out-of-scope references.
            foreach (var descendentNode in _enclosingBlock.DescendantNodes())
            {
                var descendentNodeSpanStart = descendentNode.SpanStart;
                if (descendentNodeSpanStart <= localStatementStart)
                {
                    // We're not interested in nodes that are apeared before
                    // the local declaration statement. It's either an error
                    // or not the local reference we're looking for.
                    continue;
                }

                if (descendentNodeSpanStart >= comparisonSpanStart && scopeSpan.Contains(descendentNode.Span))
                {
                    // If this is in the scope and after null-check, we don't bother checking the symbol.
                    continue;
                }

                if (descendentNode is IdentifierNameSyntax identifierName &&
                    identifierName.Identifier.ValueText == variableName &&
                    _localSymbol.Equals(_semanticModel.GetSymbolInfo(identifierName, _cancellationToken).Symbol))
                {
                    // If we got here, it means we have a local
                    // reference out of scope of the pattern variable.
                    return true;
                }
            }

            // Either no reference were found, or all
            // references were inside the given scope.
            return false;
        }

        private bool LocalFlowsIn(SyntaxNode statementOrExpression)
        {
            if (statementOrExpression == null)
            {
                return false;
            }

            if (statementOrExpression.ContainsDiagnostics)
            {
                return false;
            }

            return _semanticModel.AnalyzeDataFlow(statementOrExpression).DataFlowsIn.Contains(_localSymbol);
        }

        private bool LocalFlowsIn(StatementSyntax? firstStatement, StatementSyntax? lastStatement)
        {
            if (firstStatement == null || lastStatement == null)
            {
                return false;
            }

            if (firstStatement.ContainsDiagnostics || lastStatement.ContainsDiagnostics)
            {
                return false;
            }

            var dataFlow = _semanticModel.AnalyzeDataFlow(firstStatement, lastStatement);
            Contract.ThrowIfNull(dataFlow);
            return dataFlow.DataFlowsIn.Contains(_localSymbol);
        }
    }
}