|
' 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.
Imports System.Runtime.CompilerServices
Imports System.Threading
Imports Microsoft.CodeAnalysis.LanguageService
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions
Friend Module ParenthesizedExpressionSyntaxExtensions
Private Function EndsQuery(token As SyntaxToken, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean
Dim query = token.Parent.FirstAncestorOrSelf(Of QueryExpressionSyntax)()
If query IsNot Nothing Then
Return query.GetLastToken() = token
Else
Dim invocationAtLast = token.Parent.FirstAncestorOrSelf(Of InvocationExpressionSyntax)()
Return invocationAtLast IsNot Nothing AndAlso
invocationAtLast.GetLastToken() = token AndAlso
invocationAtLast.CanRemoveEmptyArgumentList(semanticModel) AndAlso
EndsQuery(invocationAtLast.Expression.GetLastToken(), semanticModel, cancellationToken)
End If
Return False
End Function
Private Function EndsVariableDeclarator(token As SyntaxToken) As Boolean
Dim variableDeclarator = token.Parent.FirstAncestorOrSelf(Of VariableDeclaratorSyntax)()
Return variableDeclarator IsNot Nothing AndAlso
variableDeclarator.GetLastToken() = token
End Function
Private Function EndsLambda(token As SyntaxToken) As Boolean
Dim lambda = token.Parent.FirstAncestorOrSelf(Of SingleLineLambdaExpressionSyntax)()
Return lambda IsNot Nothing AndAlso
lambda.GetLastToken() = token
End Function
<Extension>
Public Function CanRemoveParentheses(
node As ParenthesizedExpressionSyntax,
semanticModel As SemanticModel,
Optional cancellationToken As CancellationToken = Nothing
) As Boolean
If node.OpenParenToken.IsMissing OrElse node.CloseParenToken.IsMissing Then
' Cases:
' (3
Return False
End If
Dim expression = node.Expression
' Cases:
' ((Goo))
If expression.IsKind(SyntaxKind.ParenthesizedExpression) Then
Return True
End If
' ((Goo, Bar))
If expression.IsKind(SyntaxKind.TupleExpression) Then
Return True
End If
' Cases:
' ("x"c)
' (#1/1/2001#)
' (False)
' (Nothing)
' (1)
' ("")
' (True)
If expression.IsKind(SyntaxKind.CharacterLiteralExpression) OrElse
expression.IsKind(SyntaxKind.DateLiteralExpression) OrElse
expression.IsKind(SyntaxKind.FalseLiteralExpression) OrElse
expression.IsKind(SyntaxKind.NothingLiteralExpression) OrElse
expression.IsKind(SyntaxKind.NumericLiteralExpression) OrElse
expression.IsKind(SyntaxKind.StringLiteralExpression) OrElse
expression.IsKind(SyntaxKind.TrueLiteralExpression) Then
Return True
End If
' Case:
' ($"")
If expression.IsKind(SyntaxKind.InterpolatedStringExpression) Then
Return True
End If
' Cases:
' (Me)
' (MyBase)
' (MyClass)
If expression.IsKind(SyntaxKind.MeExpression) OrElse
expression.IsKind(SyntaxKind.MyBaseExpression) OrElse
expression.IsKind(SyntaxKind.MyClassExpression) Then
Return True
End If
' Cases:
' (DirectCast(Goo))
' (TryCast(Goo))
' (CType(Goo, Bar))
' (CInt(Goo))
If expression.IsKind(SyntaxKind.DirectCastExpression) OrElse
expression.IsKind(SyntaxKind.TryCastExpression) OrElse
expression.IsKind(SyntaxKind.CTypeExpression) OrElse
TypeOf expression Is PredefinedCastExpressionSyntax Then
Return True
End If
' Cases:
' (AddressOf Goo)
' (New With {.Goo = ""})
' (If(True, 1, 2))
' (If(Nothing, 1))
' (NameOf(Goo))
If expression.IsKind(SyntaxKind.AddressOfExpression) OrElse
expression.IsKind(SyntaxKind.AnonymousObjectCreationExpression) OrElse
expression.IsKind(SyntaxKind.TernaryConditionalExpression) OrElse
expression.IsKind(SyntaxKind.BinaryConditionalExpression) OrElse
expression.IsKind(SyntaxKind.NameOfExpression) Then
Return True
End If
' Cases:
' List(Of Integer()) From {({1})} -to- {({1})}
' List(Of Integer()) From {{({1})}} -to- {{{1}}}
' $"{ ({1}) } - to- $"{ {1} }"
' {({1})} -to- {({1})}
' ({1}) -to- {1}
If expression.IsKind(SyntaxKind.CollectionInitializer) Then
If node.IsParentKind(SyntaxKind.Interpolation) Then
Dim interpolation = DirectCast(node.Parent, InterpolationSyntax)
If interpolation.OpenBraceToken.Span.End = node.OpenParenToken.Span.Start AndAlso
node.OpenParenToken.Span.End = expression.Span.Start Then
' In an interpolation, we need to be careful not to remove a parenthesis if it touches a curly brace
' on the left and the right. Otherwise, code will parse differently.
Return False
End If
End If
If Not node.IsParentKind(SyntaxKind.CollectionInitializer) Then
' Standalone parenthesized array literal.
' Parentheses are insignificant.
' Ex. x = ({1})
Return True
End If
If node.Parent.IsParentKind(SyntaxKind.ObjectCollectionInitializer) AndAlso
DirectCast(node.Parent.Parent, ObjectCollectionInitializerSyntax).Initializer Is node.Parent Then
' This is a parenthesized array literal as a collection item within ObjectCollectionInitializer.
' Parentheses are significant in this case and should not be removed.
' Ex. List(Of Integer()) From {({1})}
Return False
End If
If node.Parent.IsParentKind(SyntaxKind.CollectionInitializer) AndAlso
node.Parent.Parent.IsParentKind(SyntaxKind.ObjectCollectionInitializer) AndAlso
DirectCast(node.Parent.Parent.Parent, ObjectCollectionInitializerSyntax).Initializer Is node.Parent.Parent Then
' This is a parenthesized array literal within first level sub-collection initializer within ObjectCollectionInitializer.
' Parentheses are insignificant in this case.
' Ex. List(Of Integer()) From {{({1})}}
Return True
End If
' This is a parenthesized array literal as an item expression within another array literal.
' Parentheses are significant in this case and should not be removed.
Return False
End If
Dim firstToken = expression.GetFirstToken()
Dim previousToken = node.OpenParenToken.GetPreviousToken()
' Case:
' 0 > <x/>.Value
' 0 < <x/>.Value
If firstToken.IsKind(SyntaxKind.LessThanToken) AndAlso
previousToken.IsKind(SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) Then
Return False
End If
' Cases:
' (<xml/>)
' (<xml></xml>)
' (<x/>.@a)
' (<x/>...<b>)
' (<x/>.<a>)
If expression.IsKind(SyntaxKind.XmlEmptyElement) OrElse
expression.IsKind(SyntaxKind.XmlElement) OrElse
expression.IsKind(SyntaxKind.XmlAttributeAccessExpression) OrElse
expression.IsKind(SyntaxKind.XmlDescendantAccessExpression) OrElse
expression.IsKind(SyntaxKind.XmlElementAccessExpression) Then
Return True
End If
Dim lastToken = expression.GetLastToken()
Dim nextToken = node.CloseParenToken.GetNextToken()
' Cases:
' Dim x = (Goo)
If node.IsParentKind(SyntaxKind.EqualsValue) AndAlso
Not EndsQuery(lastToken, semanticModel, cancellationToken) AndAlso
Not EndsLambda(lastToken) AndAlso
Not nextToken.IsKindOrHasMatchingText(SyntaxKind.CommaToken) Then
Return True
End If
' Cases:
' (New Goo)
' (New Goo())
If expression.IsKind(SyntaxKind.ObjectCreationExpression) Then
Dim objectCreation = DirectCast(expression, ObjectCreationExpressionSyntax)
If nextToken.IsKindOrHasMatchingText(SyntaxKind.DotToken) Then
If objectCreation.ArgumentList Is Nothing Then
' Note we can remove the parentheses when the next token is dot only
' if the type of the ObjectCreationExpression is a predefined type.
' So, we can remove parentheses in this case...
'
' Call (New Integer).ToString
'
' But not this one...
'
' Call (New Int32).ToString
Return TypeOf objectCreation.Type Is PredefinedTypeSyntax
End If
End If
If nextToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken) Then
Return False
End If
Return True
End If
' Cases:
' 1. (Goo)
' 2. (Goo())
' 3. <x/>.GetHashCode()
' 4. 1 < (<x/>.GetHashCode()) Or 1 > (<x/>.GetHashCode())
If expression.IsKind(SyntaxKind.InvocationExpression) Then
Dim invocationExpression = DirectCast(expression, InvocationExpressionSyntax)
If invocationExpression.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then
Dim memberAccess = DirectCast(invocationExpression.Expression, MemberAccessExpressionSyntax)
If (TypeOf memberAccess.Expression Is XmlNodeSyntax AndAlso
(previousToken.IsKindOrHasMatchingText(SyntaxKind.LessThanToken) OrElse
previousToken.IsKindOrHasMatchingText(SyntaxKind.GreaterThanToken))) Then
Return False
End If
End If
If invocationExpression.ArgumentList Is Nothing Then
Return Not nextToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken)
End If
Return True
End If
' Cases:
' (Goo.Bar)
' (Goo)
If expression.IsKind(SyntaxKind.IdentifierName) OrElse
expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then
' If this is a local, field or property is passed to a ByRef parameter, we should
' keep the parentheses to ensure that we don't change copy-back semantics.
If TypeOf node.Parent Is ArgumentSyntax Then
Dim symbol = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol
If symbol IsNot Nothing Then
If symbol.MatchesKind(SymbolKind.Local, SymbolKind.Field, SymbolKind.Property) Then
Dim argument = DirectCast(node.Parent, ArgumentSyntax)
Dim parameter = argument.DetermineParameter(semanticModel, cancellationToken:=cancellationToken)
If parameter IsNot Nothing AndAlso
parameter.RefKind <> RefKind.None Then
Return False
End If
End If
End If
End If
' If the next token is an open paren, we need to be careful to ensure
' that it is the opening of the argument list of a parenting invocation
' for which this is the expression.
If nextToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken) Then
If node.IsParentKind(SyntaxKind.InvocationExpression) Then
Dim parentInvocation = DirectCast(node.Parent, InvocationExpressionSyntax)
If parentInvocation.Expression Is node AndAlso
parentInvocation.ArgumentList IsNot Nothing AndAlso
parentInvocation.ArgumentList.OpenParenToken = nextToken Then
Return True
End If
End If
Return False
End If
Return True
End If
' Case:
' (Goo(Of Bar))
If expression.IsKind(SyntaxKind.GenericName) Then
If Not nextToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken) Then
Return True
End If
End If
Dim isNodeCloseParenLastTokenOfStatement = node.CloseParenToken.IsLastTokenOfStatement(checkColonTrivia:=True)
Dim nextNextToken = nextToken.GetNextToken()
' Dim z = Function() (From x In "") ' OK
' Select 1
' End Select
' Select is the only keyword in LINQ which has dual usage 1. Case selection 2. Query Select Clause
If isNodeCloseParenLastTokenOfStatement AndAlso
EndsQuery(lastToken, semanticModel, cancellationToken) AndAlso
nextToken.Kind = SyntaxKind.SelectKeyword AndAlso
nextNextToken.Kind <> SyntaxKind.CaseKeyword Then
Return False
End If
' (Await Task.Run(Function() i)) <EOL>
' (Await Task.Run(Function() i)),
If node.Expression.IsKind(SyntaxKind.AwaitExpression) AndAlso
(isNodeCloseParenLastTokenOfStatement OrElse
nextToken.Kind = SyntaxKind.CommaToken) Then
Return True
End If
' Cases:
' (1 + 1) * 8
' (1 + 1).ToString
' (1 + 1)()
If TypeOf expression Is BinaryExpressionSyntax OrElse
TypeOf expression Is UnaryExpressionSyntax Then
Dim parentExpression = TryCast(node.Parent, ExpressionSyntax)
If parentExpression IsNot Nothing Then
If parentExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression) OrElse
parentExpression.IsKind(SyntaxKind.InvocationExpression) Then
Return False
End If
Dim precedence = expression.GetOperatorPrecedence()
Dim parentPrecedence = parentExpression.GetOperatorPrecedence()
' Only remove if the expression's precedence is higher than its parent.
If parentPrecedence <> OperatorPrecedence.PrecedenceNone AndAlso
precedence < parentPrecedence Then
Return False
End If
' If the expression's precedence is the same as its parent, and both are binary expressions,
' check for associativity and commutability.
If precedence <> OperatorPrecedence.PrecedenceNone AndAlso precedence = parentPrecedence Then
Dim binaryExpression = TryCast(expression, BinaryExpressionSyntax)
Dim parentBinaryExpression = TryCast(parentExpression, BinaryExpressionSyntax)
If binaryExpression IsNot Nothing AndAlso parentBinaryExpression IsNot Nothing Then
' All binary expressions are left associative, so if the expression
' is on the left side of a binary expression the parentheses can be removed.
If parentBinaryExpression.Left Is node Then
Return True
End If
' If both the expression and its parent are binary expressions and their kinds
' are the same, and the parenthesized expression is on the right and the
' operation is associative, it can sometimes be safe to remove these parens.
'
' i.e. if you have "a AndAlso (b AndAlso c)" it can be converted to "a AndAlso b AndAlso c"
' as that New interpretation "(a AndAlso b) AndAlso 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) the result will always be the same (for logical operators, there are
' additional conditions that are checked for non-logical operators).
If IsAssociative(parentBinaryExpression.Kind) AndAlso
expression.Kind = parentExpression.Kind Then
Return VisualBasicSemanticFacts.Instance.IsSafeToChangeAssociativity(
binaryExpression, parentBinaryExpression, semanticModel)
End If
End If
Return False
End If
End If
Return True
End If
' Cases:
' (Sub() From x in y), Goo
' Dim a = (Sub() If True Then Dim x), b = a
' Dim y = (Function() Console.ReadLine)()
' Call (Sub() Exit Sub)
' Dim x = <x <%= (Sub() If True Then Else) %>/>
If TypeOf expression Is SingleLineLambdaExpressionSyntax Then
If node.CloseParenToken.IsLastTokenOfStatementWithEndOfLine() AndAlso
lastToken.Kind = SyntaxKind.ThenKeyword Then
Return False
End If
If nextToken.IsKindOrHasMatchingText(SyntaxKind.CommaToken) Then
Dim lastStatement = lastToken.Parent.GetFirstEnclosingStatement()
If EndsQuery(lastToken, semanticModel, cancellationToken) OrElse EndsVariableDeclarator(lastToken) OrElse
(EndsLambda(lastToken) AndAlso
Not previousToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken) AndAlso
lastStatement IsNot Nothing AndAlso lastStatement.Kind = SyntaxKind.ReDimStatement) Then
Return False
End If
Return True
End If
' case:
' (Sub() If True Then Dim y = Sub(z As Integer)
' End Sub).Invoke()
If nextToken.IsKindOrHasMatchingText(SyntaxKind.DotToken) AndAlso
nextToken.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then
Return False
End If
' case:
' 1. Call (Sub() If True Then Dim y = Sub(z As Integer)
' End Sub)
' 2. (Sub() If True Then Dim y = Sub()
' End Sub) Is Nothing
' 3. TypeOf (Sub() If True Then Dim y = Sub()
' End Sub) Is Object
' 4. TypeOf (Sub() If True Then Dim y = Sub()
' End Sub) IsNot Object
If (node.Parent.Kind = SyntaxKind.InvocationExpression OrElse
node.Parent.Kind = SyntaxKind.IsExpression OrElse
node.Parent.Kind = SyntaxKind.TypeOfIsExpression OrElse
node.Parent.Kind = SyntaxKind.TypeOfIsNotExpression) Then
Return False
End If
' case:
' 1. (Sub() If True Then) Implements I.A
' 2. If True Then : Dim x As Action = (Sub() If True Then) : Else : Return : End If
If nextToken.IsKindOrHasMatchingText(SyntaxKind.CloseParenToken) OrElse
nextToken.IsKindOrHasMatchingText(SyntaxKind.CloseBraceToken) OrElse
lastToken.IsLastTokenOfStatement(checkColonTrivia:=True) OrElse
node.Parent.Kind = SyntaxKind.XmlEmbeddedExpression Then
Return True
End If
' case:
' 1 .
' Dim a = Sub() If False Then Console.WriteLine() Else Dim q = From x In ""
' [Take]()
If isNodeCloseParenLastTokenOfStatement AndAlso
EndsQuery(lastToken, semanticModel, cancellationToken) AndAlso
nextToken.IsKeyword Then
Return True
End If
If isNodeCloseParenLastTokenOfStatement AndAlso
Not EndsQuery(lastToken, semanticModel, cancellationToken) Then
Return True
End If
Return False
End If
If TypeOf expression Is MultiLineLambdaExpressionSyntax Then
Return True
End If
' Cases:
' {(From x in y), From x in y}
'
' Dim q = (From x in "")
' Select 1
' End Select
'
' With New StringBuilder
' Dim q = (From x in "")
' .Length = 0
' End With
'
' Dim y = (From c In "" Distinct)
' !A = !B
If EndsQuery(lastToken, semanticModel, cancellationToken) Then
If nextToken.IsKindOrHasMatchingText(SyntaxKind.CloseParenToken) OrElse
nextToken.IsKindOrHasMatchingText(SyntaxKind.CloseBraceToken) OrElse
node.CloseParenToken.IsLastTokenOfStatement() Then
Dim nextTokenTextKind = SyntaxFacts.GetContextualKeywordKind(nextToken.Text)
Select Case nextTokenTextKind
Case SyntaxKind.AscendingKeyword,
SyntaxKind.DescendingKeyword,
SyntaxKind.DistinctKeyword,
SyntaxKind.GroupKeyword,
SyntaxKind.IntoKeyword,
SyntaxKind.OrderKeyword,
SyntaxKind.SkipKeyword,
SyntaxKind.TakeKeyword,
SyntaxKind.WhereKeyword,
SyntaxKind.JoinKeyword,
SyntaxKind.InKeyword,
SyntaxKind.LetKeyword,
SyntaxKind.OnKeyword,
SyntaxKind.SelectKeyword,
SyntaxKind.AggregateKeyword,
SyntaxKind.FromKeyword
Return False
End Select
If Not (nextToken.IsKindOrHasMatchingText(SyntaxKind.DotToken) AndAlso
nextToken.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression)) AndAlso
Not (nextToken.IsKindOrHasMatchingText(SyntaxKind.SelectKeyword) AndAlso
nextToken.Parent.IsKind(SyntaxKind.SelectStatement)) AndAlso
Not (nextToken.IsKindOrHasMatchingText(SyntaxKind.ExclamationToken) AndAlso
lastToken.IsKeyword AndAlso
nextToken.Parent.IsKind(SyntaxKind.DictionaryAccessExpression)) Then
Return True
End If
End If
Return False
End If
' case:
' (GetType(String)) => GetType(String)
If expression.IsKind(SyntaxKind.GetTypeExpression) Then
Return True
End If
' case:
' 1. (!b) => !b
If expression.Kind = SyntaxKind.DictionaryAccessExpression AndAlso
node.CloseParenToken.IsLastTokenOfStatement() Then
Return True
End If
Return False
End Function
Private Function IsAssociative(kind As SyntaxKind) As Boolean
Select Case kind
Case SyntaxKind.AddExpression,
SyntaxKind.MultiplyExpression,
SyntaxKind.AndExpression,
SyntaxKind.AndAlsoExpression,
SyntaxKind.OrExpression,
SyntaxKind.ExclusiveOrExpression,
SyntaxKind.OrElseExpression
Return True
End Select
Return False
End Function
End Module
End Namespace
|