File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Extensions\ExpressionSyntaxExtensions.cs
Web Access
Project: src\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle)
// 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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Extensions;
 
using static CSharpSyntaxTokens;
 
internal static partial class ExpressionSyntaxExtensions
{
    [return: NotNullIfNotNull(nameof(expression))]
    public static ExpressionSyntax? WalkUpParentheses(this ExpressionSyntax? expression)
    {
        while (expression?.Parent is ParenthesizedExpressionSyntax parentExpr)
            expression = parentExpr;
 
        return expression;
    }
 
    public static ExpressionSyntax WalkDownParentheses(this ExpressionSyntax expression)
    {
        while (expression is ParenthesizedExpressionSyntax parenExpression)
            expression = parenExpression.Expression;
 
        return expression;
    }
 
    public static ExpressionSyntax WalkDownSuppressions(this ExpressionSyntax expression)
    {
        while (expression is PostfixUnaryExpressionSyntax(SyntaxKind.SuppressNullableWarningExpression) postfixExpression)
            expression = postfixExpression.Operand;
 
        return expression;
    }
 
    public static bool IsQualifiedCrefName(this ExpressionSyntax expression)
        => expression.IsParentKind(SyntaxKind.NameMemberCref) && expression.Parent.IsParentKind(SyntaxKind.QualifiedCref);
 
    public static bool IsSimpleMemberAccessExpressionName([NotNullWhen(true)] this ExpressionSyntax? expression)
        => expression?.Parent is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess && memberAccess.Name == expression;
 
    public static bool IsAnyMemberAccessExpressionName([NotNullWhen(true)] this ExpressionSyntax? expression)
    {
        if (expression == null)
            return false;
 
        return expression == (expression.Parent as MemberAccessExpressionSyntax)?.Name ||
            expression.IsMemberBindingExpressionName();
    }
 
    public static bool IsMemberBindingExpressionName([NotNullWhen(true)] this ExpressionSyntax? expression)
        => expression?.Parent is MemberBindingExpressionSyntax memberBinding &&
           memberBinding.Name == expression;
 
    public static bool IsRightSideOfQualifiedName([NotNullWhen(true)] this ExpressionSyntax? expression)
        => expression?.Parent is QualifiedNameSyntax qualifiedName && qualifiedName.Right == expression;
 
    public static bool IsRightSideOfColonColon(this ExpressionSyntax expression)
        => expression?.Parent is AliasQualifiedNameSyntax aliasName && aliasName.Name == expression;
 
    public static bool IsRightSideOfDot(this ExpressionSyntax name)
        => IsSimpleMemberAccessExpressionName(name) || IsMemberBindingExpressionName(name) || IsRightSideOfQualifiedName(name) || IsQualifiedCrefName(name);
 
    public static bool IsRightSideOfDotOrArrow([NotNullWhen(true)] this ExpressionSyntax? name)
        => IsAnyMemberAccessExpressionName(name) || IsRightSideOfQualifiedName(name);
 
    public static bool IsRightSideOfDotOrColonColon(this ExpressionSyntax name)
        => IsRightSideOfDot(name) || IsRightSideOfColonColon(name);
 
    public static bool IsRightSideOfDotOrArrowOrColonColon([NotNullWhen(true)] this ExpressionSyntax name)
        => IsRightSideOfDotOrArrow(name) || IsRightSideOfColonColon(name);
 
    public static bool IsRightOfCloseParen(this ExpressionSyntax expression)
    {
        var firstToken = expression.GetFirstToken();
        return firstToken.Kind() != SyntaxKind.None
            && firstToken.GetPreviousToken().Kind() == SyntaxKind.CloseParenToken;
    }
 
    public static bool IsLeftSideOfDot([NotNullWhen(true)] this ExpressionSyntax? expression)
    {
        if (expression == null)
            return false;
 
        return IsLeftSideOfQualifiedName(expression) ||
               IsLeftSideOfSimpleMemberAccessExpression(expression);
    }
 
    public static bool IsLeftSideOfSimpleMemberAccessExpression(this ExpressionSyntax expression)
        => (expression?.Parent) is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess &&
           memberAccess.Expression == expression;
 
    public static bool IsLeftSideOfDotOrArrow(this ExpressionSyntax expression)
        => IsLeftSideOfQualifiedName(expression) ||
           (expression.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Expression == expression);
 
    public static bool IsLeftSideOfQualifiedName(this ExpressionSyntax expression)
        => (expression?.Parent) is QualifiedNameSyntax qualifiedName && qualifiedName.Left == expression;
 
    public static bool IsLeftSideOfExplicitInterfaceSpecifier([NotNullWhen(true)] this NameSyntax? name)
        => name.IsParentKind(SyntaxKind.ExplicitInterfaceSpecifier);
 
    public static bool IsExpressionOfInvocation(this ExpressionSyntax expression)
        => expression?.Parent is InvocationExpressionSyntax invocation &&
           invocation.Expression == expression;
 
    public static bool TryGetNameParts(this ExpressionSyntax expression, [NotNullWhen(true)] out IList<string>? parts)
    {
        var partsList = new List<string>();
        if (!TryGetNameParts(expression, partsList))
        {
            parts = null;
            return false;
        }
 
        parts = partsList;
        return true;
    }
 
    public static bool TryGetNameParts(this ExpressionSyntax expression, List<string> parts)
    {
        if (expression is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess)
        {
            if (!TryGetNameParts(memberAccess.Expression, parts))
            {
                return false;
            }
 
            return AddSimpleName(memberAccess.Name, parts);
        }
        else if (expression is QualifiedNameSyntax qualifiedName)
        {
            if (!TryGetNameParts(qualifiedName.Left, parts))
            {
                return false;
            }
 
            return AddSimpleName(qualifiedName.Right, parts);
        }
        else if (expression is SimpleNameSyntax simpleName)
        {
            return AddSimpleName(simpleName, parts);
        }
        else
        {
            return false;
        }
    }
 
    private static bool AddSimpleName(SimpleNameSyntax simpleName, List<string> parts)
    {
        if (!simpleName.IsKind(SyntaxKind.IdentifierName))
        {
            return false;
        }
 
        parts.Add(simpleName.Identifier.ValueText);
        return true;
    }
 
    public static bool IsInConstantContext([NotNullWhen(true)] this ExpressionSyntax? expression)
    {
        if (expression == null)
            return false;
 
        if (expression.GetAncestor<ParameterSyntax>() != null)
            return true;
 
        var attributeArgument = expression.GetAncestor<AttributeArgumentSyntax>();
        if (attributeArgument != null)
        {
            if (attributeArgument.NameEquals == null ||
                expression != attributeArgument.NameEquals.Name)
            {
                return true;
            }
        }
 
        if (expression.IsParentKind(SyntaxKind.ConstantPattern))
            return true;
 
        // note: the above list is not intended to be exhaustive.  If more cases
        // are discovered that should be considered 'constant' contexts in the
        // language, then this should be updated accordingly.
        return false;
    }
 
    public static bool IsInOutContext([NotNullWhen(true)] this ExpressionSyntax? expression)
        => expression?.Parent is ArgumentSyntax { RefOrOutKeyword: SyntaxToken(SyntaxKind.OutKeyword) } argument &&
           argument.Expression == expression;
 
    public static bool IsInRefContext([NotNullWhen(true)] this ExpressionSyntax? expression)
        => IsInRefContext(expression, out _);
 
    /// <summary>
    /// Returns true if this expression is in some <c>ref</c> keyword context.  If <see langword="true"/> then
    /// <paramref name="refParent"/> will be the node containing the <see langword="ref"/> keyword.
    /// </summary>
    public static bool IsInRefContext([NotNullWhen(true)] this ExpressionSyntax? expression, [NotNullWhen(true)] out SyntaxNode? refParent)
    {
        while (expression?.Parent is ParenthesizedExpressionSyntax or PostfixUnaryExpressionSyntax(SyntaxKind.SuppressNullableWarningExpression))
            expression = (ExpressionSyntax)expression.Parent;
 
        if (expression?.Parent is RefExpressionSyntax or
                                  ArgumentSyntax { RefOrOutKeyword.RawKind: (int)SyntaxKind.RefKeyword })
        {
            refParent = expression.Parent;
            return true;
        }
 
        refParent = null;
        return false;
    }
 
    public static bool IsInInContext([NotNullWhen(true)] this ExpressionSyntax? expression)
        => expression?.Parent is ArgumentSyntax { RefKindKeyword: SyntaxToken(SyntaxKind.InKeyword) };
 
    [return: NotNullIfNotNull(nameof(expression))]
    private static ExpressionSyntax? GetExpressionToAnalyzeForWrites(ExpressionSyntax? expression)
    {
        if (expression.IsRightSideOfDotOrArrow())
        {
            expression = (ExpressionSyntax)expression.GetRequiredParent();
        }
 
        expression = expression.WalkUpParentheses();
 
        return expression;
    }
 
    public static bool IsOnlyWrittenTo([NotNullWhen(true)] this ExpressionSyntax? expression)
    {
        expression = GetExpressionToAnalyzeForWrites(expression);
 
        if (expression != null)
        {
            if (expression.IsInOutContext())
                return true;
 
            if (expression.Parent != null)
            {
                if (expression.IsLeftSideOfAssignExpression())
                    return true;
 
                if (expression.IsAttributeNamedArgumentIdentifier())
                    return true;
            }
 
            if (IsExpressionOfArgumentInDeconstruction(expression))
            {
                return true;
            }
        }
 
        return false;
    }
 
    /// <summary>
    /// If this declaration or identifier is part of a deconstruction, find the deconstruction.
    /// If found, returns either an assignment expression or a foreach variable statement.
    /// Returns null otherwise.
    ///
    /// copied from SyntaxExtensions.GetContainingDeconstruction
    /// </summary>
    private static bool IsExpressionOfArgumentInDeconstruction(ExpressionSyntax expr)
    {
        if (!expr.IsParentKind(SyntaxKind.Argument))
        {
            return false;
        }
 
        while (true)
        {
            var parent = expr.Parent;
            if (parent == null)
            {
                return false;
            }
 
            switch (parent.Kind())
            {
                case SyntaxKind.Argument:
                    if (parent.Parent?.Kind() == SyntaxKind.TupleExpression)
                    {
                        expr = (TupleExpressionSyntax)parent.Parent;
                        continue;
                    }
 
                    return false;
                case SyntaxKind.SimpleAssignmentExpression:
                    if (((AssignmentExpressionSyntax)parent).Left == expr)
                    {
                        return true;
                    }
 
                    return false;
                case SyntaxKind.ForEachVariableStatement:
                    if (((ForEachVariableStatementSyntax)parent).Variable == expr)
                    {
                        return true;
                    }
 
                    return false;
 
                default:
                    return false;
            }
        }
    }
 
    public static bool IsWrittenTo(
        [NotNullWhen(true)] this ExpressionSyntax? expression,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        if (expression == null)
            return false;
 
        expression = GetExpressionToAnalyzeForWrites(expression);
 
        if (expression.IsOnlyWrittenTo())
            return true;
 
        if (expression.IsInRefContext(out var refParent))
        {
            // most cases of `ref x` will count as a potential write of `x`.  An important exception is:
            // `ref readonly y = ref x`.  In that case, because 'y' can't be written to, this would not 
            // be a write of 'x'.
            if (refParent.Parent is EqualsValueClauseSyntax { Parent: VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type: { } variableDeclarationType } } })
            {
                if (variableDeclarationType is ScopedTypeSyntax scopedType)
                {
                    variableDeclarationType = scopedType.Type;
                }
 
                if (variableDeclarationType is RefTypeSyntax refType && refType.ReadOnlyKeyword != default)
                {
                    return false;
                }
            }
 
            return true;
        }
 
        // Similar to `ref x`, `&x` allows reads and write of the value, meaning `x` may be (but is not definitely)
        // written to.
        if (expression.Parent.IsKind(SyntaxKind.AddressOfExpression))
            return true;
 
        // We're written if we're used in a ++, or -- expression.
        if (expression.IsOperandOfIncrementOrDecrementExpression())
            return true;
 
        if (expression.IsLeftSideOfAnyAssignExpression())
            return true;
 
        // An extension method invocation with a ref-this parameter can write to an expression.
        if (expression.Parent is MemberAccessExpressionSyntax memberAccess &&
            expression == memberAccess.Expression)
        {
            var symbol = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).Symbol;
            if (symbol is IMethodSymbol
                {
                    MethodKind: MethodKind.ReducedExtension,
                    ReducedFrom.Parameters: [{ RefKind: RefKind.Ref }, ..],
                })
            {
                return true;
            }
        }
 
        // An inline array passed as a Span<T> can be written into by the callee, despite no ref at the callsite.  e.g.:
        //
        // void Mutate(Span<byte> bytes);
        // Mutate(this.inlineArray)
        if (expression.Parent is ArgumentSyntax)
        {
            var expressionTypes = semanticModel.GetTypeInfo(expression, cancellationToken);
            if (expressionTypes.ConvertedType.IsSpan() &&
                expressionTypes.Type.IsInlineArray())
            {
                return true;
            }
        }
 
        return false;
    }
 
    public static bool IsAttributeNamedArgumentIdentifier([NotNullWhen(true)] this ExpressionSyntax? expression)
    {
        var nameEquals = expression?.Parent as NameEqualsSyntax;
        return nameEquals.IsParentKind(SyntaxKind.AttributeArgument);
    }
 
    public static bool IsOperandOfIncrementOrDecrementExpression(this ExpressionSyntax expression)
    {
        if (expression?.Parent is SyntaxNode parent)
        {
            switch (parent.Kind())
            {
                case SyntaxKind.PostIncrementExpression:
                case SyntaxKind.PreIncrementExpression:
                case SyntaxKind.PostDecrementExpression:
                case SyntaxKind.PreDecrementExpression:
                    return true;
            }
        }
 
        return false;
    }
 
    public static bool IsNamedArgumentIdentifier(this ExpressionSyntax expression)
        => expression is IdentifierNameSyntax { Parent: NameColonSyntax };
 
    public static bool IsInsideNameOfExpression(
        [NotNullWhen(true)] this ExpressionSyntax? expression, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        var invocation = expression?.GetAncestor<InvocationExpressionSyntax>();
        if (invocation?.Expression is IdentifierNameSyntax name &&
            name.Identifier.Text == SyntaxFacts.GetText(SyntaxKind.NameOfKeyword))
        {
            return semanticModel.GetMemberGroup(name, cancellationToken).IsDefaultOrEmpty;
        }
 
        return false;
    }
 
    private static bool CanReplace(ISymbol symbol)
    {
        switch (symbol.Kind)
        {
            case SymbolKind.Field:
            case SymbolKind.Local:
            case SymbolKind.Method:
            case SymbolKind.Parameter:
            case SymbolKind.Property:
            case SymbolKind.RangeVariable:
            case SymbolKind.FunctionPointerType:
                return true;
        }
 
        return false;
    }
 
    public static bool CanReplaceWithRValue(
        [NotNullWhen(true)] this ExpressionSyntax? expression, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        // An RValue can't be written into.
        // i.e. you can't replace "a" in "a = b" with "Goo() = b".
        return
            expression != null &&
            !expression.IsWrittenTo(semanticModel, cancellationToken) &&
            CanReplaceWithLValue(expression, semanticModel, cancellationToken);
    }
 
    public static bool CanReplaceWithLValue(
        this ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        if (expression.IsKind(SyntaxKind.StackAllocArrayCreationExpression))
        {
            // Stack alloc is very interesting.  While it appears to be an expression, it is only
            // such so it can appear in a variable decl.  It is not a normal expression that can
            // go anywhere.
            return false;
        }
 
        if (expression.Kind()
                is SyntaxKind.BaseExpression
                or SyntaxKind.CollectionInitializerExpression
                or SyntaxKind.ObjectInitializerExpression
                or SyntaxKind.ComplexElementInitializerExpression)
        {
            return false;
        }
 
        // literal can be always replaced.
        if (expression is LiteralExpressionSyntax && !expression.IsParentKind(SyntaxKind.UnaryMinusExpression))
        {
            return true;
        }
 
        if (expression is TupleExpressionSyntax)
        {
            return true;
        }
 
        if (!(expression is ObjectCreationExpressionSyntax) &&
            !(expression is AnonymousObjectCreationExpressionSyntax) &&
            !expression.IsLeftSideOfAssignExpression())
        {
            var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken);
            if (!symbolInfo.GetBestOrAllSymbols().All(CanReplace))
            {
                // If the expression is actually a reference to a type, then it can't be replaced
                // with an arbitrary expression.
                return false;
            }
        }
 
        // If we are a conditional access expression:
        // case (1) : obj?.Method(), obj1.obj2?.Property
        // case (2) : obj?.GetAnotherObj()?.Length, obj?.AnotherObj?.Length
        // in case (1), the entire expression forms the conditional access expression, which can be replaced with an LValue.
        // in case (2), the nested conditional access expression is ".GetAnotherObj()?.Length" or ".AnotherObj()?.Length"
        // essentially, the first expression (before the operator) in a nested conditional access expression
        // is some form of member binding expression and they cannot be replaced with an LValue.
        if (expression.IsKind(SyntaxKind.ConditionalAccessExpression))
        {
            return expression is { Parent.RawKind: not (int)SyntaxKind.ConditionalAccessExpression };
        }
 
        if (expression.Parent == null)
            return false;
 
        switch (expression.Parent.Kind())
        {
            case SyntaxKind.InvocationExpression:
                // Technically, you could introduce an LValue for "Goo" in "Goo()" even if "Goo" binds
                // to a method.  (i.e. by assigning to a Func<...> type).  However, this is so contrived
                // and none of the features that use this extension consider this replaceable.
                if (expression.IsKind(SyntaxKind.IdentifierName) || expression is MemberAccessExpressionSyntax)
                {
                    // If it looks like a method then we don't allow it to be replaced if it is a
                    // method (or if it doesn't bind).
 
                    var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken);
                    return symbolInfo.GetBestOrAllSymbols().Any() && !symbolInfo.GetBestOrAllSymbols().Any(static s => s is IMethodSymbol);
                }
                else
                {
                    // It doesn't look like a method, we allow this to be replaced.
                    return true;
                }
 
            // If the parent is a conditional access expression, we could introduce an LValue
            // for the given expression, unless it is itself a MemberBindingExpression or starts with one.
            // Case (1) : The WhenNotNull clause always starts with a MemberBindingExpression.
            //              expression '.Method()' in a?.Method()
            // Case (2) : The Expression clause always starts with a MemberBindingExpression if
            // the grandparent is a conditional access expression.
            //              expression '.Method' in a?.Method()?.Length
            // Case (3) : The child Conditional access expression always starts with a MemberBindingExpression if
            // the parent is a conditional access expression. This case is already covered before the parent kind switch
            case SyntaxKind.ConditionalAccessExpression:
                var parentConditionalAccessExpression = (ConditionalAccessExpressionSyntax)expression.Parent;
                return expression != parentConditionalAccessExpression.WhenNotNull &&
                        !parentConditionalAccessExpression.Parent.IsKind(SyntaxKind.ConditionalAccessExpression);
 
            case SyntaxKind.IsExpression:
            case SyntaxKind.AsExpression:
                // Can't introduce a variable for the type portion of an is/as check.
                var isOrAsExpression = (BinaryExpressionSyntax)expression.Parent;
                return expression == isOrAsExpression.Left;
            case SyntaxKind.EqualsValueClause:
            case SyntaxKind.ExpressionStatement:
            case SyntaxKind.ArrayInitializerExpression:
            case SyntaxKind.CollectionInitializerExpression:
            case SyntaxKind.Argument:
            case SyntaxKind.AttributeArgument:
            case SyntaxKind.AnonymousObjectMemberDeclarator:
            case SyntaxKind.ArrowExpressionClause:
            case SyntaxKind.AwaitExpression:
            case SyntaxKind.ReturnStatement:
            case SyntaxKind.YieldReturnStatement:
            case SyntaxKind.ParenthesizedLambdaExpression:
            case SyntaxKind.SimpleLambdaExpression:
            case SyntaxKind.ParenthesizedExpression:
            case SyntaxKind.ArrayRankSpecifier:
            case SyntaxKind.ConditionalExpression:
            case SyntaxKind.IfStatement:
            case SyntaxKind.CatchFilterClause:
            case SyntaxKind.WhileStatement:
            case SyntaxKind.DoStatement:
            case SyntaxKind.ThrowStatement:
            case SyntaxKind.SwitchStatement:
            case SyntaxKind.InterpolatedStringExpression:
            case SyntaxKind.ComplexElementInitializerExpression:
            case SyntaxKind.Interpolation:
            case SyntaxKind.RefExpression:
            case SyntaxKind.LockStatement:
            case SyntaxKind.ElementAccessExpression:
            case SyntaxKind.SwitchExpressionArm:
            case SyntaxKind.WhenClause:
                // Direct parent kind checks.
                return true;
        }
 
        if (expression.Parent is PrefixUnaryExpressionSyntax)
        {
            if (!(expression is LiteralExpressionSyntax && expression.IsParentKind(SyntaxKind.UnaryMinusExpression)))
            {
                return true;
            }
        }
 
        var parentNonExpression = expression.GetAncestors().SkipWhile(n => n is ExpressionSyntax).FirstOrDefault();
        var topExpression = expression;
        while (topExpression.Parent is TypeSyntax typeSyntax)
        {
            topExpression = typeSyntax;
        }
 
        if (parentNonExpression != null &&
            parentNonExpression is FromClauseSyntax fromClause &&
            topExpression != null &&
            fromClause.Type == topExpression)
        {
            return false;
        }
 
        // Parent type checks.
        if (expression.Parent is PostfixUnaryExpressionSyntax or
            BinaryExpressionSyntax or
            AssignmentExpressionSyntax or
            QueryClauseSyntax or
            SelectOrGroupClauseSyntax or
            CheckedExpressionSyntax)
        {
            return true;
        }
 
        // Specific child checks.
        if (expression.CheckParent<CommonForEachStatementSyntax>(f => f.Expression == expression) ||
            expression.CheckParent<MemberAccessExpressionSyntax>(m => m.Expression == expression) ||
            expression.CheckParent<CastExpressionSyntax>(c => c.Expression == expression))
        {
            return true;
        }
 
        // Misc checks.
        if ((expression.IsParentKind(SyntaxKind.NameEquals) && expression.Parent.IsParentKind(SyntaxKind.AttributeArgument)) ||
            expression.IsLeftSideOfAnyAssignExpression())
        {
            return true;
        }
 
        return false;
    }
 
    public static bool IsNameOfArgumentExpression(this ExpressionSyntax expression)
        => expression is { Parent: ArgumentSyntax { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax invocation } } } &&
           invocation.IsNameOfInvocation();
 
    public static bool IsNameOfInvocation(this InvocationExpressionSyntax invocation)
    {
        return invocation.Expression is IdentifierNameSyntax identifierName &&
               identifierName.Identifier.IsKindOrHasMatchingText(SyntaxKind.NameOfKeyword);
    }
 
    public static SimpleNameSyntax? GetRightmostName(this ExpressionSyntax node)
    {
        if (node is MemberAccessExpressionSyntax memberAccess && memberAccess.Name != null)
        {
            return memberAccess.Name;
        }
 
        if (node is QualifiedNameSyntax qualified && qualified.Right != null)
        {
            return qualified.Right;
        }
 
        if (node is SimpleNameSyntax simple)
        {
            return simple;
        }
 
        if (node is ConditionalAccessExpressionSyntax conditional)
        {
            return conditional.WhenNotNull.GetRightmostName();
        }
 
        if (node is MemberBindingExpressionSyntax memberBinding)
        {
            return memberBinding.Name;
        }
 
        if (node is AliasQualifiedNameSyntax aliasQualifiedName && aliasQualifiedName.Name != null)
        {
            return aliasQualifiedName.Name;
        }
 
        return null;
    }
 
    public static OperatorPrecedence GetOperatorPrecedence(this ExpressionSyntax expression)
    {
        switch (expression.Kind())
        {
            case SyntaxKind.SimpleMemberAccessExpression:
            case SyntaxKind.ConditionalAccessExpression:
            case SyntaxKind.InvocationExpression:
            case SyntaxKind.ElementAccessExpression:
            case SyntaxKind.PostIncrementExpression:
            case SyntaxKind.PostDecrementExpression:
            case SyntaxKind.ObjectCreationExpression:
            case SyntaxKind.ImplicitObjectCreationExpression:
            case SyntaxKind.TypeOfExpression:
            case SyntaxKind.DefaultExpression:
            case SyntaxKind.CheckedExpression:
            case SyntaxKind.UncheckedExpression:
            case SyntaxKind.AnonymousMethodExpression:
            case SyntaxKind.SuppressNullableWarningExpression:
            // unsafe code
            case SyntaxKind.SizeOfExpression:
            case SyntaxKind.PointerMemberAccessExpression:
                // From C# spec, 7.3.1:
                // Primary: x.y  x?.y  x?[y]  f(x)  a[x]  x++  x--  new  typeof  default  checked  unchecked  delegate  x! 
 
                return OperatorPrecedence.Primary;
 
            case SyntaxKind.UnaryPlusExpression:
            case SyntaxKind.UnaryMinusExpression:
            case SyntaxKind.LogicalNotExpression:
            case SyntaxKind.BitwiseNotExpression:
            case SyntaxKind.PreIncrementExpression:
            case SyntaxKind.PreDecrementExpression:
            case SyntaxKind.CastExpression:
            case SyntaxKind.AwaitExpression:
            // unsafe code.
            case SyntaxKind.PointerIndirectionExpression:
            case SyntaxKind.AddressOfExpression:
 
                // From C# spec, 7.3.1:
                // Unary: +  -  !  ~  ++x  --x  (T)x  await Task
 
                return OperatorPrecedence.Unary;
 
            case SyntaxKind.RangeExpression:
                // From C# spec, https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#systemrange
                // Range: ..
 
                return OperatorPrecedence.Range;
 
            case SyntaxKind.MultiplyExpression:
            case SyntaxKind.DivideExpression:
            case SyntaxKind.ModuloExpression:
                // From C# spec, 7.3.1:
                // Multiplicative: *  /  %
 
                return OperatorPrecedence.Multiplicative;
 
            case SyntaxKind.AddExpression:
            case SyntaxKind.SubtractExpression:
                // From C# spec, 7.3.1:
                // Additive: +  -
 
                return OperatorPrecedence.Additive;
 
            case SyntaxKind.LeftShiftExpression:
            case SyntaxKind.RightShiftExpression:
                // From C# spec, 7.3.1:
                // Shift: <<  >>
 
                return OperatorPrecedence.Shift;
 
            case SyntaxKind.LessThanExpression:
            case SyntaxKind.GreaterThanExpression:
            case SyntaxKind.LessThanOrEqualExpression:
            case SyntaxKind.GreaterThanOrEqualExpression:
            case SyntaxKind.IsExpression:
            case SyntaxKind.AsExpression:
            case SyntaxKind.IsPatternExpression:
                // From C# spec, 7.3.1:
                // Relational and type testing: <  >  <=  >=  is  as
 
                return OperatorPrecedence.RelationalAndTypeTesting;
 
            case SyntaxKind.EqualsExpression:
            case SyntaxKind.NotEqualsExpression:
                // From C# spec, 7.3.1:
                // Equality: ==  !=
 
                return OperatorPrecedence.Equality;
 
            case SyntaxKind.BitwiseAndExpression:
                // From C# spec, 7.3.1:
                // Logical AND: &
 
                return OperatorPrecedence.LogicalAnd;
 
            case SyntaxKind.ExclusiveOrExpression:
                // From C# spec, 7.3.1:
                // Logical XOR: ^
 
                return OperatorPrecedence.LogicalXor;
 
            case SyntaxKind.BitwiseOrExpression:
                // From C# spec, 7.3.1:
                // Logical OR: |
 
                return OperatorPrecedence.LogicalOr;
 
            case SyntaxKind.LogicalAndExpression:
                // From C# spec, 7.3.1:
                // Conditional AND: &&
 
                return OperatorPrecedence.ConditionalAnd;
 
            case SyntaxKind.LogicalOrExpression:
                // From C# spec, 7.3.1:
                // Conditional AND: ||
 
                return OperatorPrecedence.ConditionalOr;
 
            case SyntaxKind.CoalesceExpression:
                // From C# spec, 7.3.1:
                // Null coalescing: ??
 
                return OperatorPrecedence.NullCoalescing;
 
            case SyntaxKind.ConditionalExpression:
                // From C# spec, 7.3.1:
                // Conditional: ?:
 
                return OperatorPrecedence.Conditional;
 
            case SyntaxKind.SimpleAssignmentExpression:
            case SyntaxKind.MultiplyAssignmentExpression:
            case SyntaxKind.DivideAssignmentExpression:
            case SyntaxKind.ModuloAssignmentExpression:
            case SyntaxKind.AddAssignmentExpression:
            case SyntaxKind.SubtractAssignmentExpression:
            case SyntaxKind.LeftShiftAssignmentExpression:
            case SyntaxKind.RightShiftAssignmentExpression:
            case SyntaxKind.AndAssignmentExpression:
            case SyntaxKind.ExclusiveOrAssignmentExpression:
            case SyntaxKind.OrAssignmentExpression:
            case SyntaxKind.SimpleLambdaExpression:
            case SyntaxKind.ParenthesizedLambdaExpression:
                // From C# spec, 7.3.1:
                // Conditional: ?:
 
                return OperatorPrecedence.AssignmentAndLambdaExpression;
 
            case SyntaxKind.SwitchExpression:
                return OperatorPrecedence.Switch;
 
            default:
                return OperatorPrecedence.None;
        }
    }
 
    public static bool TryConvertToStatement(
        this ExpressionSyntax expression,
        SyntaxToken? semicolonTokenOpt,
        bool createReturnStatementForExpression,
        [NotNullWhen(true)] out StatementSyntax? statement)
    {
        // It's tricky to convert an arrow expression with directives over to a block.
        // We'd need to find and remove the directives *after* the arrow expression and
        // move them accordingly.  So, for now, we just disallow this.
        if (expression.GetLeadingTrivia().Any(t => t.IsDirective))
        {
            statement = null;
            return false;
        }
 
        var semicolonToken = semicolonTokenOpt ?? SemicolonToken;
 
        statement = ConvertToStatement(expression, semicolonToken, createReturnStatementForExpression);
        return true;
    }
 
    private static StatementSyntax ConvertToStatement(ExpressionSyntax expression, SyntaxToken semicolonToken, bool createReturnStatementForExpression)
    {
        if (expression is ThrowExpressionSyntax throwExpression)
        {
            return SyntaxFactory.ThrowStatement(throwExpression.ThrowKeyword, throwExpression.Expression, semicolonToken);
        }
        else if (createReturnStatementForExpression)
        {
            if (expression.GetLeadingTrivia().Any(t => t.IsSingleOrMultiLineComment()))
            {
                return SyntaxFactory.ReturnStatement(expression.WithLeadingTrivia(SyntaxFactory.ElasticSpace))
                                    .WithSemicolonToken(semicolonToken)
                                    .WithLeadingTrivia(expression.GetLeadingTrivia())
                                    .WithPrependedLeadingTrivia(SyntaxFactory.ElasticMarker);
            }
            else
            {
                return SyntaxFactory.ReturnStatement(expression)
                                    .WithSemicolonToken(semicolonToken);
            }
        }
        else
        {
            return SyntaxFactory.ExpressionStatement(expression)
                                .WithSemicolonToken(semicolonToken);
        }
    }
 
    public static bool IsDirectChildOfMemberAccessExpression(this ExpressionSyntax expression)
        => expression?.Parent is MemberAccessExpressionSyntax;
 
    public static bool InsideCrefReference(this ExpressionSyntax expression)
        => expression.FirstAncestorOrSelf<XmlCrefAttributeSyntax>() != null;
 
    public static ITypeSymbol? GetTargetType(
        this ExpressionSyntax expression,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        var topExpression = expression.WalkUpParentheses();
        var parent = topExpression.Parent;
        return parent switch
        {
            EqualsValueClauseSyntax equalsValue => GetTargetTypeForEqualsValueClause(equalsValue),
            CastExpressionSyntax castExpression => GetTargetTypedForCastExpression(castExpression),
            // a ? [1, 2, 3] : ...  is target typed if either the other side is *not* a collection,
            // or the entire ternary is target typed itself.
            ConditionalExpressionSyntax conditionalExpression => GetTargetTypeForConditionalExpression(conditionalExpression, topExpression),
            // Similar rules for switches.
            SwitchExpressionArmSyntax switchExpressionArm => GetTargetTypeForSwitchExpressionArm(switchExpressionArm),
            InitializerExpressionSyntax initializerExpression => GetTargetTypeForInitializerExpression(initializerExpression, topExpression),
            CollectionElementSyntax collectionElement => GetTargetTypeForCollectionElement(collectionElement),
            AssignmentExpressionSyntax assignmentExpression => GetTargetTypeForAssignmentExpression(assignmentExpression, topExpression),
            BinaryExpressionSyntax binaryExpression => GetTargetTypeForBinaryExpression(binaryExpression, topExpression),
            LambdaExpressionSyntax lambda => GetTargetTypeForLambdaExpression(lambda, topExpression),
            ArgumentSyntax argument => GetTargetTypeForArgument(argument),
            AttributeArgumentSyntax attributeArgument => GetTargetTypeForAttributeArgument(attributeArgument),
            ReturnStatementSyntax returnStatement => GetTargetTypeForReturnStatement(returnStatement),
            ArrowExpressionClauseSyntax arrowExpression => GetTargetTypeForArrowExpression(arrowExpression),
            _ => null,
        };
 
        // return result is IErrorTypeSymbol ? null : result;
 
        bool HasType(ExpressionSyntax expression, [NotNullWhen(true)] out ITypeSymbol? type)
        {
            type = semanticModel.GetTypeInfo(expression, cancellationToken).Type;
            return type is not null; // and not IErrorTypeSymbol;
        }
 
        ITypeSymbol? GetTargetTypeForArgument(ArgumentSyntax argument)
        {
            if (argument.Parent is TupleExpressionSyntax tupleExpression)
            {
                var tupleType = tupleExpression.GetTargetType(semanticModel, cancellationToken);
                if (tupleType is null)
                    return null;
 
                var typeArguments = tupleType.GetTypeArguments();
                var index = tupleExpression.Arguments.IndexOf(argument);
 
                return index < typeArguments.Length ? typeArguments[index] : null;
            }
            else
            {
                return argument.DetermineParameter(semanticModel, allowUncertainCandidates: false, allowParams: true, cancellationToken)?.Type;
            }
        }
 
        ITypeSymbol? GetTargetTypeForAttributeArgument(AttributeArgumentSyntax argument)
            => argument.DetermineParameter(semanticModel, allowUncertainCandidates: false, allowParams: true, cancellationToken)?.Type;
 
        ITypeSymbol? GetTargetTypeForArrowExpression(ArrowExpressionClauseSyntax arrowExpression)
        {
            var parent = arrowExpression.GetRequiredParent();
            var symbol = semanticModel.GetSymbolInfo(parent, cancellationToken).Symbol ?? semanticModel.GetDeclaredSymbol(parent, cancellationToken);
            return symbol.GetMemberType();
        }
 
        ITypeSymbol? GetTargetTypeForReturnStatement(ReturnStatementSyntax returnStatement)
        {
            for (SyntaxNode? current = returnStatement; current != null; current = current.Parent)
            {
                if (current.IsReturnableConstruct())
                {
                    var symbol = semanticModel.GetSymbolInfo(current, cancellationToken).Symbol ?? semanticModel.GetDeclaredSymbol(current, cancellationToken);
                    return symbol.GetMemberType();
                }
            }
 
            return null;
        }
 
        ITypeSymbol? GetTargetTypeForEqualsValueClause(EqualsValueClauseSyntax equalsValue)
        {
            // If we're after an `x = ...` and it's not `var x`, this is target typed.
            if (equalsValue.Parent is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type.IsVar: true } })
                return null;
 
            var symbol = semanticModel.GetDeclaredSymbol(equalsValue.GetRequiredParent(), cancellationToken);
            return symbol.GetMemberType();
        }
 
        ITypeSymbol? GetTargetTypedForCastExpression(CastExpressionSyntax castExpression)
            => semanticModel.GetTypeInfo(castExpression.Type, cancellationToken).Type;
 
        ITypeSymbol? GetTargetTypeForConditionalExpression(ConditionalExpressionSyntax conditionalExpression, ExpressionSyntax expression)
        {
            if (conditionalExpression.WhenTrue == expression)
                return HasType(conditionalExpression.WhenFalse, out var falseType) ? falseType : conditionalExpression.GetTargetType(semanticModel, cancellationToken);
            else if (conditionalExpression.WhenFalse == expression)
                return HasType(conditionalExpression.WhenTrue, out var trueType) ? trueType : conditionalExpression.GetTargetType(semanticModel, cancellationToken);
            else
                return null;
        }
 
        ITypeSymbol? GetTargetTypeForLambdaExpression(LambdaExpressionSyntax lambda, ExpressionSyntax expression)
            => lambda.ExpressionBody == expression &&
               lambda.GetTargetType(semanticModel, cancellationToken) is INamedTypeSymbol { DelegateInvokeMethod.ReturnType: var returnType } ? returnType : null;
 
        ITypeSymbol? GetTargetTypeForSwitchExpressionArm(SwitchExpressionArmSyntax switchExpressionArm)
        {
            var switchExpression = (SwitchExpressionSyntax)switchExpressionArm.GetRequiredParent();
 
            // check if any other arm has a type that this would be target typed against.
            foreach (var arm in switchExpression.Arms)
            {
                if (arm != switchExpressionArm && HasType(arm.Expression, out var armType))
                    return armType;
            }
 
            // All arms do not have a type, this is target typed if the switch itself is target typed.
            return switchExpression.GetTargetType(semanticModel, cancellationToken);
        }
 
        ITypeSymbol? GetTargetTypeForCollectionElement(CollectionElementSyntax collectionElement)
        {
            // We do not currently target type spread expressions in a collection expression.
            if (collectionElement is not ExpressionElementSyntax)
                return null;
 
            // The element it target typed if the parent collection is itself target typed.
            var collectionExpression = (CollectionExpressionSyntax)collectionElement.GetRequiredParent();
            var collectionTargetType = collectionExpression.GetTargetType(semanticModel, cancellationToken);
            if (collectionTargetType is null)
                return null;
 
            if (collectionTargetType.IsSpanOrReadOnlySpan())
                return collectionTargetType.GetTypeArguments().Single();
 
            var ienumerableType = semanticModel.Compilation.IEnumerableOfTType();
            if (collectionTargetType.OriginalDefinition.Equals(ienumerableType))
                return collectionTargetType.GetTypeArguments().Single();
 
            foreach (var iface in collectionTargetType.AllInterfaces)
            {
                if (iface.OriginalDefinition.Equals(ienumerableType))
                    return iface.TypeArguments.Single();
            }
 
            return null;
        }
 
        ITypeSymbol? GetTargetTypeForInitializerExpression(InitializerExpressionSyntax initializerExpression, ExpressionSyntax expression)
        {
            // new X[] { [1, 2, 3] }.  Elements are target typed by array type.
            if (initializerExpression.Parent is ArrayCreationExpressionSyntax arrayCreation)
                return HasType(arrayCreation.Type, out var elementType) ? elementType : null;
 
            // new [] { [1, 2, 3], ... }.  Elements are target typed if there's another element with real type.
            if (initializerExpression.Parent is ImplicitArrayCreationExpressionSyntax)
            {
                foreach (var sibling in initializerExpression.Expressions)
                {
                    if (sibling != expression && HasType(sibling, out var siblingType))
                        return siblingType;
                }
 
                return null;
            }
 
            // T[] x = [1, 2, 3];
            if (initializerExpression.Parent is EqualsValueClauseSyntax equalsValue)
                return GetTargetTypeForEqualsValueClause(equalsValue);
 
            // TODO: Handle these.
            if (initializerExpression.Parent is StackAllocArrayCreationExpressionSyntax or ImplicitStackAllocArrayCreationExpressionSyntax)
                return null;
 
            return null;
        }
 
        ITypeSymbol? GetTargetTypeForAssignmentExpression(AssignmentExpressionSyntax assignmentExpression, ExpressionSyntax expression)
        {
            return expression == assignmentExpression.Right && HasType(assignmentExpression.Left, out var leftType) ? leftType : null;
        }
 
        ITypeSymbol? GetTargetTypeForBinaryExpression(BinaryExpressionSyntax binaryExpression, ExpressionSyntax expression)
        {
            return binaryExpression.Kind() == SyntaxKind.CoalesceExpression && binaryExpression.Right == expression && HasType(binaryExpression.Left, out var leftType) ? leftType : null;
        }
    }
}