|
// 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;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
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"/>. 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(
ISemanticFacts semanticFacts,
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(
semanticFacts, semanticModel, rootOperation, declaredSymbol, rootOperation.Syntax.Span, includeDeclaration: true, cancellationToken) == true;
}
/// <summary>
/// Given an operation, goes through all descendent 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(
ISemanticFacts semanticFacts,
SemanticModel semanticModel,
IOperation rootOperation,
ISymbol symbol,
TextSpan span,
bool includeDeclaration,
CancellationToken cancellationToken)
{
var hasReference = false;
using var _ = ArrayBuilder<IOperation>.GetInstance(out var stack);
stack.Push(rootOperation);
while (stack.TryPop(out var operation))
{
if (!span.IntersectsWith(operation.Syntax.Span))
continue;
if (span.Contains(operation.Syntax.Span) &&
IsSymbolReferencedByOperation(operation))
{
hasReference = true;
if (operation is IAssignmentOperation assignmentOperation &&
assignmentOperation.Syntax.RawKind == semanticFacts.SyntaxFacts.SyntaxKinds.SimpleAssignmentExpression)
{
// IsSymbolReferencedByOperation will ensure that the reference is the target of the assignment.
//
// Note: we care about the value after the assignment, so we have to check the RHS to see if maybe-null
// is flowing in. In other words `currentlyNotNull = maybeNUll;` will be maybe-null *after* the
// assignment. and should cause our caller to keep the type as nullable.
var typeInfo = semanticModel.GetTypeInfo(assignmentOperation.Value.Syntax, cancellationToken);
if (IsMaybeNull(typeInfo))
return true;
// We specifically are not recursing down the left side of this variable. If we have `x = not-null`
// then 'x' maybe-null in flowing in, but we care about what it is when flowing out, which the above
// check handled.
stack.Push(assignmentOperation.Value);
continue;
}
// foreach statements are handled special because the iterator is not assignable, so the element type
// annotation is accurate for determining if the loop declaration has a reference that allows the symbol to
// be null
if (operation is IForEachLoopOperation forEachLoop)
{
var foreachInfo = semanticFacts.GetForEachSymbols(semanticModel, forEachLoop.Syntax);
// 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 != null && foreachInfo.ElementType.NullableAnnotation != NullableAnnotation.NotAnnotated)
return true;
// intentional fall through.
}
else if (operation is IVariableDeclaratorOperation variableDeclarator)
{
// IsSymbolReferencedByOperation ensures that GetVariableInitializer() returns a non-null value
var syntax = variableDeclarator.GetVariableInitializer()!.Value.Syntax;
var typeInfo = semanticModel.GetTypeInfo(syntax, cancellationToken);
if (IsMaybeNull(typeInfo))
return true;
// intentional fall through.
}
else
{
var typeInfo = semanticModel.GetTypeInfo(operation.Syntax, cancellationToken);
if (IsMaybeNull(typeInfo))
return true;
// intentional fall through.
}
}
foreach (var childOperation in operation.ChildOperations.Reverse())
stack.Push(childOperation);
}
return hasReference ? false : null;
static bool IsMaybeNull(TypeInfo typeInfo)
=> typeInfo.Nullability.FlowState == NullableFlowState.MaybeNull;
// <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>
bool IsSymbolReferencedByOperation(IOperation operation)
=> operation switch
{
ILocalReferenceOperation localReference => localReference.Local.Equals(symbol),
IParameterReferenceOperation parameterReference => parameterReference.Parameter.Equals(symbol),
IAssignmentOperation assignment => IsSymbolReferencedByOperation(assignment.Target),
ITupleOperation tupleOperation => tupleOperation.Elements.Any(IsSymbolReferencedByOperation),
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 when includeDeclaration => variableDeclarator.GetVariableInitializer() != null && variableDeclarator.Symbol.Equals(symbol),
_ => false
};
}
}
|