|
// 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.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 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 SyntaxNode _localStatement;
private readonly SyntaxNode _enclosingBlock;
private readonly CancellationToken _cancellationToken;
private Analyzer(
SemanticModel semanticModel,
ILocalSymbol localSymbol,
ExpressionSyntax comparison,
ExpressionSyntax operand,
SyntaxNode localStatement,
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;
_enclosingBlock = enclosingBlock;
_cancellationToken = cancellationToken;
}
public static bool CanSafelyConvertToPatternMatching(
SemanticModel semanticModel,
ILocalSymbol localSymbol,
ExpressionSyntax comparison,
ExpressionSyntax operand,
SyntaxNode localStatement,
SyntaxNode enclosingBlock,
CancellationToken cancellationToken)
{
var analyzer = new Analyzer(semanticModel, localSymbol, comparison, operand, localStatement, enclosingBlock, cancellationToken);
return analyzer.CanSafelyConvertToPatternMatching();
}
// 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.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 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);
}
}
}
}
|