File: ParenthesizedExpressionSyntaxExtensions.cs
Web Access
Project: ..\..\..\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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Extensions
{
    internal static class ParenthesizedExpressionSyntaxExtensions
    {
        public static bool CanRemoveParentheses(
            this ParenthesizedExpressionSyntax node, SemanticModel semanticModel, CancellationToken cancellationToken)
        {
            if (node.OpenParenToken.IsMissing || node.CloseParenToken.IsMissing)
            {
                // int x = (3;
                return false;
            }
 
            var nodeParent = node.Parent;
            if (nodeParent == null)
                return false;
 
            var expression = node.Expression;
 
            // The 'direct' expression that contains this parenthesized node.  Note: in the case
            // of code like: ```x is (y)``` there is an intermediary 'no-syntax' 'ConstantPattern'
            // node between the 'is-pattern' node and the parenthesized expression.  So we manually
            // jump past that as, for all intents and purposes, we want to consider the 'is' expression
            // as the parent expression of the (y) expression.
            var parentExpression = nodeParent.IsKind(SyntaxKind.ConstantPattern)
                ? nodeParent.Parent as ExpressionSyntax
                : nodeParent as ExpressionSyntax;
 
            // Have to be careful if we would remove parens and cause a + and a + to become a ++.
            // (same with - as well).
            var tokenBeforeParen = node.GetFirstToken().GetPreviousToken();
            var tokenAfterParen = node.Expression.GetFirstToken();
            var previousChar = tokenBeforeParen.Text.LastOrDefault();
            var nextChar = tokenAfterParen.Text.FirstOrDefault();
 
            if ((previousChar == '+' && nextChar == '+') ||
                (previousChar == '-' && nextChar == '-'))
            {
                return false;
            }
 
            // Simplest cases:
            //   ((x)) -> (x)
            if (expression.IsKind(SyntaxKind.ParenthesizedExpression) ||
                parentExpression.IsKind(SyntaxKind.ParenthesizedExpression))
            {
                return true;
            }
 
            if (expression is StackAllocArrayCreationExpressionSyntax or ImplicitStackAllocArrayCreationExpressionSyntax)
            {
                // var span = (stackalloc byte[8]);
                // https://github.com/dotnet/roslyn/issues/44629
                // The code semantics changes if the parenthesis removed.
                // With parenthesis:    variable span is of type `Span<byte>`.
                // Without parenthesis: variable span is of type `byte*` which can only be used in unsafe context.
                if (nodeParent is EqualsValueClauseSyntax { Parent: VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax varDecl } })
                {
                    // We have either `var x = (stackalloc byte[8])` or `Span<byte> x = (stackalloc byte[8])`.  The former
                    // is not safe to remove. the latter is.
                    if (semanticModel.GetTypeInfo(varDecl.Type, cancellationToken).Type is
                        {
                            Name: nameof(Span<int>) or nameof(ReadOnlySpan<int>),
                            ContainingNamespace: { Name: nameof(System), ContainingNamespace.IsGlobalNamespace: true }
                        })
                    {
                        return !varDecl.Type.IsVar;
                    }
                }
 
                return false;
            }
 
            // Don't remove parentheses around `<` and `>` if there's a reasonable chance that it might
            // pair with the opposite form, causing them to be reinterpreted as generic syntax. See
            // https://github.com/dotnet/roslyn/issues/43934 for examples.
            if (expression.Kind() is SyntaxKind.GreaterThanExpression or SyntaxKind.LessThanExpression &&
                nodeParent is ArgumentSyntax)
            {
                var opposite = expression.IsKind(SyntaxKind.GreaterThanExpression) ? SyntaxKind.LessThanExpression : SyntaxKind.GreaterThanExpression;
                if (nodeParent.GetRequiredParent().ChildNodes().OfType<ArgumentSyntax>().Any(a => a.Expression.IsKind(opposite)))
                    return false;
            }
 
            // (throw ...) -> throw ...
            if (expression.IsKind(SyntaxKind.ThrowExpression))
                return true;
 
            // (x); -> x;
            if (nodeParent.IsKind(SyntaxKind.ExpressionStatement))
                return true;
 
            // => (x)   ->   => x
            if (nodeParent.IsKind(SyntaxKind.ArrowExpressionClause))
                return true;
 
            // checked((x)) -> checked(x)
            if (nodeParent.Kind() is SyntaxKind.CheckedExpression or SyntaxKind.UncheckedExpression)
                return true;
 
            // ((x, y)) -> (x, y)
            if (expression.IsKind(SyntaxKind.TupleExpression))
                return true;
 
            // int Prop => (x); -> int Prop => x;
            if (nodeParent is ArrowExpressionClauseSyntax arrowExpressionClause && arrowExpressionClause.Expression == node)
            {
                return true;
            }
 
            // Easy statement-level cases:
            //   var y = (x);           -> var y = x;
            //   var (y, z) = (x);      -> var (y, z) = x;
            //   if ((x))               -> if (x)
            //   return (x);            -> return x;
            //   yield return (x);      -> yield return x;
            //   throw (x);             -> throw x;
            //   switch ((x))           -> switch (x)
            //   while ((x))            -> while (x)
            //   do { } while ((x))     -> do { } while (x)
            //   for(;(x);)             -> for(;x;)
            //   foreach (var y in (x)) -> foreach (var y in x)
            //   lock ((x))             -> lock (x)
            //   using ((x))            -> using (x)
            //   catch when ((x))       -> catch when (x)
            if ((nodeParent is EqualsValueClauseSyntax equalsValue && equalsValue.Value == node) ||
                (nodeParent is IfStatementSyntax ifStatement && ifStatement.Condition == node) ||
                (nodeParent is ReturnStatementSyntax returnStatement && returnStatement.Expression == node) ||
                (nodeParent is YieldStatementSyntax(SyntaxKind.YieldReturnStatement) yieldStatement && yieldStatement.Expression == node) ||
                (nodeParent is ThrowStatementSyntax throwStatement && throwStatement.Expression == node) ||
                (nodeParent is SwitchStatementSyntax switchStatement && switchStatement.Expression == node) ||
                (nodeParent is WhileStatementSyntax whileStatement && whileStatement.Condition == node) ||
                (nodeParent is DoStatementSyntax doStatement && doStatement.Condition == node) ||
                (nodeParent is ForStatementSyntax forStatement && forStatement.Condition == node) ||
                (nodeParent is CommonForEachStatementSyntax forEachStatement && forEachStatement.Expression == node) ||
                (nodeParent is LockStatementSyntax lockStatement && lockStatement.Expression == node) ||
                (nodeParent is UsingStatementSyntax usingStatement && usingStatement.Expression == node) ||
                (nodeParent is CatchFilterClauseSyntax catchFilter && catchFilter.FilterExpression == node))
            {
                return true;
            }
 
            // Handle expression-level ambiguities
            if (RemovalMayIntroduceCastAmbiguity(node) ||
                RemovalMayIntroduceCommaListAmbiguity(node) ||
                RemovalMayIntroduceInterpolationAmbiguity(node) ||
                RemovalWouldChangeConstantReferenceToTypeReference(node, expression, semanticModel, cancellationToken))
            {
                return false;
            }
 
            // Cases:
            //   (C)(this) -> (C)this
            if (nodeParent.IsKind(SyntaxKind.CastExpression) && expression.IsKind(SyntaxKind.ThisExpression))
                return true;
 
            // Cases:
            //   y((x)) -> y(x)
            if (nodeParent is ArgumentSyntax argument && argument.Expression == node)
                return true;
 
            // Cases:
            //   $"{(x)}" -> $"{x}"
            if (nodeParent.IsKind(SyntaxKind.Interpolation))
                return true;
 
            // Cases:
            //   ($"{x}") -> $"{x}"
            if (expression.IsKind(SyntaxKind.InterpolatedStringExpression))
                return true;
 
            // Cases:
            //   {(x)} -> {x}
            if (nodeParent is InitializerExpressionSyntax)
            {
                // Assignment expressions are not allowed in initializers
                if (expression.IsAnyAssignExpression())
                    return false;
 
                return true;
            }
 
            // Cases:
            //   new {(x)} -> {x}
            //   new { a = (x)} -> { a = x }
            //   new { a = (x = c)} -> { a = x = c }
            if (nodeParent is AnonymousObjectMemberDeclaratorSyntax anonymousDeclarator)
            {
                // Assignment expressions are not allowed unless member is named
                if (anonymousDeclarator.NameEquals == null && expression.IsAnyAssignExpression())
                    return false;
 
                return true;
            }
 
            // Cases:
            // where (x + 1 > 14) -> where x + 1 > 14
            if (nodeParent is QueryClauseSyntax)
                return true;
 
            // Cases:
            //   (x)   -> x
            //   (x.y) -> x.y
            if (IsSimpleOrDottedName(expression))
                return true;
 
            // Cases:
            //   ('')      -> ''
            //   ("")      -> ""
            //   (false)   -> false
            //   (true)    -> true
            //   (null)    -> null
            //   (default) -> default;
            //   (1)       -> 1
            if (expression.IsAnyLiteralExpression())
                return true;
 
            // (this)   -> this
            if (expression.IsKind(SyntaxKind.ThisExpression))
                return true;
 
            // x ?? (throw ...) -> x ?? throw ...
            if (expression.IsKind(SyntaxKind.ThrowExpression) &&
                nodeParent is BinaryExpressionSyntax(SyntaxKind.CoalesceExpression) binary &&
                binary.Right == node)
            {
                return true;
            }
 
            // case (x): -> case x:
            if (nodeParent.IsKind(SyntaxKind.CaseSwitchLabel))
                return true;
 
            // case (x) when y: -> case x when y:
            if (nodeParent.IsKind(SyntaxKind.ConstantPattern) &&
                nodeParent.IsParentKind(SyntaxKind.CasePatternSwitchLabel))
            {
                return true;
            }
 
            // case x when (y): -> case x when y:
            if (nodeParent.IsKind(SyntaxKind.WhenClause))
                return true;
 
            // #if (x)   ->   #if x
            if (nodeParent is DirectiveTriviaSyntax)
                return true;
 
            // Switch expression arm
            // x => (y)
            if (nodeParent is SwitchExpressionArmSyntax arm && arm.Expression == node)
                return true;
 
            // If we have: (X)(++x) or (X)(--x), we don't want to remove the parens. doing so can
            // make the ++/-- now associate with the previous part of the cast expression.
            if (parentExpression.IsKind(SyntaxKind.CastExpression))
            {
                if (expression.IsKind(SyntaxKind.PreIncrementExpression) ||
                    expression.IsKind(SyntaxKind.PreDecrementExpression))
                {
                    return false;
                }
            }
 
            // (condition ? ref a : ref b ) = SomeValue, parenthesis can't be removed for when conditional expression appears at left
            // This syntax is only allowed since C# 7.2
            if (expression.IsKind(SyntaxKind.ConditionalExpression) &&
                node.IsLeftSideOfAnyAssignExpression())
            {
                return false;
            }
 
            // Don't change (x?.Count)... to x?.Count...
            //
            // It very much changes the semantics to have code that always executed (outside the
            // parenthesized expression) now only conditionally run depending on if 'x' is null or
            // not.
            if (expression.IsKind(SyntaxKind.ConditionalAccessExpression))
                return false;
 
            // Operator precedence cases:
            // - If the parent is not an expression, do not remove parentheses
            // - Otherwise, parentheses may be removed if doing so does not change operator associations.
            return parentExpression != null && !RemovalChangesAssociation(node, parentExpression, semanticModel);
        }
 
        private static bool RemovalWouldChangeConstantReferenceToTypeReference(
            ParenthesizedExpressionSyntax node, ExpressionSyntax expression,
            SemanticModel semanticModel, CancellationToken cancellationToken)
        {
            // With cases like: `if (x is (Y))` then we cannot remove the parens if it would make Y now bind to a type
            // instead of a constant.
            if (node.Parent is not ConstantPatternSyntax { Parent: IsPatternExpressionSyntax })
                return false;
 
            var exprSymbol = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol;
            if (exprSymbol is not IFieldSymbol { IsConst: true } field)
                return false;
 
            // See if interpreting the same expression as a type in this location binds.
            var potentialType = semanticModel.GetSpeculativeTypeInfo(expression.SpanStart, expression, SpeculativeBindingOption.BindAsTypeOrNamespace).Type;
            return potentialType is not (null or IErrorTypeSymbol);
        }
 
        private static readonly ObjectPool<Stack<SyntaxNode>> s_nodeStackPool = SharedPools.Default<Stack<SyntaxNode>>();
 
        private static bool RemovalMayIntroduceInterpolationAmbiguity(ParenthesizedExpressionSyntax node)
        {
            // First, find the parenting interpolation. If we find a parenthesize expression first,
            // we can bail out early.
            InterpolationSyntax? interpolation = null;
            foreach (var ancestor in node.GetRequiredParent().AncestorsAndSelf())
            {
                if (ancestor.IsKind(SyntaxKind.ParenthesizedExpression))
                    return false;
 
                if (ancestor.IsKind(SyntaxKind.Interpolation, out interpolation))
                    break;
            }
 
            if (interpolation == null)
                return false;
 
            // In order determine whether removing this parenthesized expression will introduce a
            // parsing ambiguity, we must dig into the child tokens and nodes to determine whether
            // they include any : or :: tokens. If they do, we can't remove the parentheses because
            // the parser would assume that the first : would begin the format clause of the interpolation.
 
            var stack = s_nodeStackPool.AllocateAndClear();
            try
            {
                stack.Push(node.Expression);
 
                while (stack.Count > 0)
                {
                    var expression = stack.Pop();
 
                    foreach (var nodeOrToken in expression.ChildNodesAndTokens())
                    {
                        // Note: There's no need drill into other parenthesized expressions, since any colons in them would be unambiguous.
                        if (nodeOrToken.IsNode && !nodeOrToken.IsKind(SyntaxKind.ParenthesizedExpression))
                        {
                            stack.Push(nodeOrToken.AsNode()!);
                        }
                        else if (nodeOrToken.IsToken)
                        {
                            if (nodeOrToken.IsKind(SyntaxKind.ColonToken) || nodeOrToken.IsKind(SyntaxKind.ColonColonToken))
                            {
                                return true;
                            }
                        }
                    }
                }
            }
            finally
            {
                s_nodeStackPool.ClearAndFree(stack);
            }
 
            return false;
        }
 
        private static bool RemovalChangesAssociation(
            ParenthesizedExpressionSyntax node, ExpressionSyntax parentExpression, SemanticModel semanticModel)
        {
            var expression = node.Expression;
            var precedence = expression.GetOperatorPrecedence();
            var parentPrecedence = parentExpression.GetOperatorPrecedence();
            if (precedence == OperatorPrecedence.None || parentPrecedence == OperatorPrecedence.None)
            {
                // Be conservative if the expression or its parent has no precedence.
                return true;
            }
 
            if (precedence > parentPrecedence)
            {
                // Association never changes if the expression's precedence is higher than its parent.
                return false;
            }
            else if (precedence < parentPrecedence)
            {
                // Association always changes if the expression's precedence is lower that its parent.
                return true;
            }
            else if (precedence == parentPrecedence)
            {
                // If the expression's precedence is the same as its parent, and both are binary expressions,
                // check for associativity and commutability.
 
                if (expression is not (BinaryExpressionSyntax or AssignmentExpressionSyntax))
                {
                    // If the expression is not a binary expression, association never changes.
                    return false;
                }
 
                if (parentExpression is BinaryExpressionSyntax parentBinaryExpression)
                {
                    // If both the expression and its parent are binary expressions and their kinds
                    // are the same, and the parenthesized expression is on hte right and the 
                    // operation is associative, it can sometimes be safe to remove these parens.
                    //
                    // i.e. if you have "a && (b && c)" it can be converted to "a && b && c" 
                    // as that new interpretation "(a && b) && c" operates the exact same way at 
                    // runtime.
                    //
                    // Specifically: 
                    //  1) the operands are still executed in the same order: a, b, then c.
                    //     So even if they have side effects, it will not matter.
                    //  2) the same shortcircuiting happens.
                    //  3) for logical operators the result will always be the same (there are 
                    //     additional conditions that are checked for non-logical operators).
                    if (IsAssociative(parentBinaryExpression.Kind()) &&
                        parentBinaryExpression.Right == node &&
                        node.Expression.IsKind(parentBinaryExpression.Kind(), out BinaryExpressionSyntax? nodeBinary))
                    {
                        return !CSharpSemanticFacts.Instance.IsSafeToChangeAssociativity(
                            nodeBinary, parentBinaryExpression, semanticModel);
                    }
 
                    // Null-coalescing is right associative; removing parens from the LHS changes the association.
                    if (parentExpression.IsKind(SyntaxKind.CoalesceExpression))
                    {
                        return parentBinaryExpression.Left == node;
                    }
 
                    // All other binary operators are left associative; removing parens from the RHS changes the association.
                    return parentBinaryExpression.Right == node;
                }
 
                if (parentExpression is AssignmentExpressionSyntax parentAssignmentExpression)
                {
                    // Assignment expressions are right associative; removing parens from the LHS changes the association.
                    return parentAssignmentExpression.Left == node;
                }
 
                // If the parent is not a binary expression, association never changes.
                return false;
            }
 
            throw ExceptionUtilities.Unreachable();
        }
 
        private static bool IsAssociative(SyntaxKind kind)
        {
            switch (kind)
            {
                case SyntaxKind.AddExpression:
                case SyntaxKind.MultiplyExpression:
                case SyntaxKind.BitwiseOrExpression:
                case SyntaxKind.ExclusiveOrExpression:
                case SyntaxKind.LogicalOrExpression:
                case SyntaxKind.BitwiseAndExpression:
                case SyntaxKind.LogicalAndExpression:
                    return true;
            }
 
            return false;
        }
 
        private static bool RemovalMayIntroduceCastAmbiguity(ParenthesizedExpressionSyntax node)
        {
            // Be careful not to break the special case around (x)(-y)
            // as defined in section 7.7.6 of the C# language specification.
            //
            // cases we can't remove the parens for are:
            //
            //      (x)(+y)
            //      (x)(-y)
            //      (x)(&y) // unsafe code
            //      (x)(*y) // unsafe code
            //
            // Note: we can remove the parens if the (x) part is unambiguously a type.
            // i.e. if it something like:
            //
            //      (int)(...)
            //      (x[])(...)
            //      (X*)(...)
            //      (X?)(...)
            //      (global::X)(...)
 
            if (node?.Parent is CastExpressionSyntax castExpression)
            {
                if (castExpression.Type.Kind() is
                        SyntaxKind.PredefinedType or
                        SyntaxKind.ArrayType or
                        SyntaxKind.PointerType or
                        SyntaxKind.NullableType)
                {
                    return false;
                }
 
                if (castExpression.Type is NameSyntax name && StartsWithAlias(name))
                {
                    return false;
                }
 
                var expression = node.Expression;
 
                if (expression.Kind() is
                        SyntaxKind.UnaryMinusExpression or
                        SyntaxKind.UnaryPlusExpression or
                        SyntaxKind.PointerIndirectionExpression or
                        SyntaxKind.AddressOfExpression)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        private static bool StartsWithAlias(NameSyntax name)
        {
            if (name.IsKind(SyntaxKind.AliasQualifiedName))
            {
                return true;
            }
 
            if (name is QualifiedNameSyntax qualifiedName)
            {
                return StartsWithAlias(qualifiedName.Left);
            }
 
            return false;
        }
 
        private static bool RemovalMayIntroduceCommaListAmbiguity(ParenthesizedExpressionSyntax node)
        {
            if (IsSimpleOrDottedName(node.Expression))
            {
                // We can't remove parentheses from an identifier name in the following cases:
                //   F((x) < x, x > (1 + 2))
                //   F(x < (x), x > (1 + 2))
                //   F(x < x, (x) > (1 + 2))
                //   {(x) < x, x > (1 + 2)}
                //   {x < (x), x > (1 + 2)}
                //   {x < x, (x) > (1 + 2)}
 
                if (node.Parent is BinaryExpressionSyntax binaryExpression &&
                    binaryExpression.Kind() is SyntaxKind.LessThanExpression or SyntaxKind.GreaterThanExpression &&
                    (binaryExpression.IsParentKind(SyntaxKind.Argument) || binaryExpression.Parent is InitializerExpressionSyntax))
                {
                    if (binaryExpression.IsKind(SyntaxKind.LessThanExpression))
                    {
                        if ((binaryExpression.Left == node && IsSimpleOrDottedName(binaryExpression.Right)) ||
                            (binaryExpression.Right == node && IsSimpleOrDottedName(binaryExpression.Left)))
                        {
                            if (IsNextExpressionPotentiallyAmbiguous(binaryExpression))
                            {
                                return true;
                            }
                        }
 
                        return false;
                    }
                    else if (binaryExpression.IsKind(SyntaxKind.GreaterThanExpression))
                    {
                        if (binaryExpression.Left == node &&
                            binaryExpression.Right.Kind() is SyntaxKind.ParenthesizedExpression or SyntaxKind.CastExpression)
                        {
                            if (IsPreviousExpressionPotentiallyAmbiguous(binaryExpression))
                            {
                                return true;
                            }
                        }
 
                        return false;
                    }
                }
            }
            else if (node.Expression.IsKind(SyntaxKind.LessThanExpression))
            {
                // We can't remove parentheses from a less-than expression in the following cases:
                //   F((x < x), x > (1 + 2))
                //   {(x < x), x > (1 + 2)}
                return IsNextExpressionPotentiallyAmbiguous(node);
            }
            else if (node.Expression.IsKind(SyntaxKind.GreaterThanExpression))
            {
                // We can't remove parentheses from a greater-than expression in the following cases:
                //   F(x < x, (x > (1 + 2)))
                //   {x < x, (x > (1 + 2))}
                return IsPreviousExpressionPotentiallyAmbiguous(node);
            }
 
            return false;
        }
 
        private static bool IsPreviousExpressionPotentiallyAmbiguous(ExpressionSyntax node)
        {
            ExpressionSyntax? previousExpression = null;
 
            if (node.Parent is ArgumentSyntax argument)
            {
                if (argument.Parent is ArgumentListSyntax argumentList)
                {
                    var argumentIndex = argumentList.Arguments.IndexOf(argument);
                    if (argumentIndex > 0)
                    {
                        previousExpression = argumentList.Arguments[argumentIndex - 1].Expression;
                    }
                }
            }
            else if (node.Parent is InitializerExpressionSyntax initializer)
            {
                var expressionIndex = initializer.Expressions.IndexOf(node);
                if (expressionIndex > 0)
                {
                    previousExpression = initializer.Expressions[expressionIndex - 1];
                }
            }
 
            if (previousExpression == null ||
                previousExpression is not BinaryExpressionSyntax(SyntaxKind.LessThanExpression) lessThanExpression)
            {
                return false;
            }
 
            return (IsSimpleOrDottedName(lessThanExpression.Left)
                    || lessThanExpression.Left.IsKind(SyntaxKind.CastExpression))
                && IsSimpleOrDottedName(lessThanExpression.Right);
        }
 
        private static bool IsNextExpressionPotentiallyAmbiguous(ExpressionSyntax node)
        {
            ExpressionSyntax? nextExpression = null;
 
            if (node.Parent is ArgumentSyntax argument)
            {
                if (argument.Parent is ArgumentListSyntax argumentList)
                {
                    var argumentIndex = argumentList.Arguments.IndexOf(argument);
                    if (argumentIndex >= 0 && argumentIndex < argumentList.Arguments.Count - 1)
                    {
                        nextExpression = argumentList.Arguments[argumentIndex + 1].Expression;
                    }
                }
            }
            else if (node.Parent is InitializerExpressionSyntax initializer)
            {
                var expressionIndex = initializer.Expressions.IndexOf(node);
                if (expressionIndex >= 0 && expressionIndex < initializer.Expressions.Count - 1)
                {
                    nextExpression = initializer.Expressions[expressionIndex + 1];
                }
            }
 
            if (nextExpression == null ||
                nextExpression is not BinaryExpressionSyntax(SyntaxKind.GreaterThanExpression) greaterThanExpression)
            {
                return false;
            }
 
            return IsSimpleOrDottedName(greaterThanExpression.Left)
                && greaterThanExpression.Right.Kind() is SyntaxKind.ParenthesizedExpression or SyntaxKind.CastExpression;
        }
 
        private static bool IsSimpleOrDottedName(ExpressionSyntax expression)
            => expression.Kind() is SyntaxKind.IdentifierName or SyntaxKind.QualifiedName or SyntaxKind.SimpleMemberAccessExpression;
 
        public static bool CanRemoveParentheses(this ParenthesizedPatternSyntax node)
        {
            if (node.OpenParenToken.IsMissing || node.CloseParenToken.IsMissing)
            {
                // int x = (3;
                return false;
            }
 
            var pattern = node.Pattern;
 
            // We wrap a parenthesized pattern and we're parenthesized.  We can remove our parens.
            if (pattern is ParenthesizedPatternSyntax)
                return true;
 
            // We're parenthesized discard pattern. We cannot remove parens.
            // x is (_)
            if (pattern is DiscardPatternSyntax && node.Parent is IsPatternExpressionSyntax)
                return false;
 
            // (not ...) -> not ...
            //
            // this is safe because unary patterns have the highest precedence, so even if you had:
            // (not ...) or (not ...)
            //
            // you can safely convert to `not ... or not ...`
            var patternPrecedence = pattern.GetOperatorPrecedence();
            if (patternPrecedence is OperatorPrecedence.Primary or OperatorPrecedence.Unary)
                return true;
 
            // We're parenthesized and are inside a parenthesized pattern.  We can remove our parens.
            // ((x)) -> (x)
            if (node.Parent is ParenthesizedPatternSyntax)
                return true;
 
            // x is (...)  ->  x is ...
            if (node.Parent is IsPatternExpressionSyntax)
                return true;
 
            // (x or y) => ...  ->    x or y => ...
            if (node.Parent is SwitchExpressionArmSyntax)
                return true;
 
            // X: (y or z)      ->    X: y or z
            if (node.Parent is SubpatternSyntax)
                return true;
 
            // case (x or y):   ->    case x or y:
            if (node.Parent is CasePatternSwitchLabelSyntax)
                return true;
 
            // Operator precedence cases:
            // - If the parent is not an expression, do not remove parentheses
            // - Otherwise, parentheses may be removed if doing so does not change operator associations.
            return node.Parent is PatternSyntax patternParent &&
                   !RemovalChangesAssociation(node, patternParent);
        }
 
        private static bool RemovalChangesAssociation(
            ParenthesizedPatternSyntax node, PatternSyntax parentPattern)
        {
            var pattern = node.Pattern;
            var precedence = pattern.GetOperatorPrecedence();
            var parentPrecedence = parentPattern.GetOperatorPrecedence();
            if (precedence == OperatorPrecedence.None || parentPrecedence == OperatorPrecedence.None)
            {
                // Be conservative if the expression or its parent has no precedence.
                return true;
            }
 
            // Association always changes if the expression's precedence is lower that its parent.
            return precedence < parentPrecedence;
        }
 
        public static OperatorPrecedence GetOperatorPrecedence(this PatternSyntax pattern)
        {
            switch (pattern)
            {
                case ConstantPatternSyntax _:
                case DiscardPatternSyntax _:
                case DeclarationPatternSyntax _:
                case RecursivePatternSyntax _:
                case TypePatternSyntax _:
                case VarPatternSyntax _:
                    return OperatorPrecedence.Primary;
 
                case UnaryPatternSyntax _:
                case RelationalPatternSyntax _:
                    return OperatorPrecedence.Unary;
 
                case BinaryPatternSyntax binaryPattern:
                    if (binaryPattern.IsKind(SyntaxKind.AndPattern))
                        return OperatorPrecedence.ConditionalAnd;
 
                    if (binaryPattern.IsKind(SyntaxKind.OrPattern))
                        return OperatorPrecedence.ConditionalOr;
 
                    break;
            }
 
            Debug.Fail("Unhandled pattern type");
            return OperatorPrecedence.None;
        }
    }
}