// 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 CanSafelyConvertToPatternMatching() { // 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.IsKind(SyntaxKind.NotEqualsExpression, 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); }