File: Simplification\Simplifiers\NameSimplifier.vb
Web Access
Project: src\src\Workspaces\VisualBasic\Portable\Microsoft.CodeAnalysis.VisualBasic.Workspaces.vbproj (Microsoft.CodeAnalysis.VisualBasic.Workspaces)
' 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.InteropServices
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.CodeStyle
Imports Microsoft.CodeAnalysis.Options
Imports Microsoft.CodeAnalysis.Rename.ConflictEngine
Imports Microsoft.CodeAnalysis.Simplification
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Imports Microsoft.CodeAnalysis.VisualBasic.Utilities
 
Namespace Microsoft.CodeAnalysis.VisualBasic.Simplification.Simplifiers
    Friend Class NameSimplifier
        Inherits AbstractVisualBasicSimplifier(Of NameSyntax, ExpressionSyntax)
 
        Public Shared ReadOnly Instance As New NameSimplifier()
 
        Private Sub New()
        End Sub
 
        Public Overrides Function TrySimplify(
                name As NameSyntax,
                semanticModel As SemanticModel,
                options As VisualBasicSimplifierOptions,
                <Out> ByRef replacementNode As ExpressionSyntax,
                <Out> ByRef issueSpan As TextSpan,
                cancellationToken As CancellationToken) As Boolean
 
            ' do not simplify names of a namespace declaration
            If IsPartOfNamespaceDeclarationName(name) Then
                Return False
            End If
 
            ' see whether binding the name binds to a symbol/type. if not, it is ambiguous and
            ' nothing we can do here.
            Dim symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, name)
            If SimplificationHelpers.IsValidSymbolInfo(symbol) Then
                If symbol.Kind = SymbolKind.Method AndAlso symbol.IsConstructor() Then
                    symbol = symbol.ContainingType
                End If
 
                If symbol.Kind = SymbolKind.Method AndAlso name.Kind = SyntaxKind.GenericName Then
                    Dim genericName = DirectCast(name, GenericNameSyntax)
                    replacementNode = SyntaxFactory.IdentifierName(genericName.Identifier).WithLeadingTrivia(genericName.GetLeadingTrivia()).WithTrailingTrivia(genericName.GetTrailingTrivia())
 
                    issueSpan = genericName.TypeArgumentList.Span
                    Return CanReplaceWithReducedName(name, replacementNode, semanticModel, cancellationToken)
                End If
 
                If Not TypeOf symbol Is INamespaceOrTypeSymbol Then
                    Return False
                End If
            Else
                Return False
            End If
 
            If name.HasAnnotations(SpecialTypeAnnotation.Kind) Then
                replacementNode = SyntaxFactory.PredefinedType(
                    SyntaxFactory.Token(name.GetLeadingTrivia(),
                                 GetPredefinedKeywordKind(SpecialTypeAnnotation.GetSpecialType(name.GetAnnotations(SpecialTypeAnnotation.Kind).First())),
                                 name.GetTrailingTrivia()))
 
                issueSpan = name.Span
 
                Return CanReplaceWithReducedNameInContext(name, replacementNode)
            Else
 
                If Not name.IsRightSideOfDot() Then
 
                    Dim aliasReplacement As IAliasSymbol = Nothing
                    If TryReplaceWithAlias(name, semanticModel, aliasReplacement) Then
                        Dim identifierToken = SyntaxFactory.Identifier(
                                name.GetLeadingTrivia(),
                                aliasReplacement.Name,
                                name.GetTrailingTrivia())
 
                        identifierToken = TryEscapeIdentifierToken(identifierToken)
 
                        replacementNode = SyntaxFactory.IdentifierName(identifierToken)
 
                        Dim annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(RenameAnnotation.Kind)
                        For Each annotatedNodeOrToken In annotatedNodesOrTokens
                            If annotatedNodeOrToken.IsToken Then
                                identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken)
                            Else
                                replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode)
                            End If
                        Next
 
                        annotatedNodesOrTokens = name.GetAnnotatedNodesAndTokens(AliasAnnotation.Kind)
                        For Each annotatedNodeOrToken In annotatedNodesOrTokens
                            If annotatedNodeOrToken.IsToken Then
                                identifierToken = annotatedNodeOrToken.AsToken().CopyAnnotationsTo(identifierToken)
                            Else
                                replacementNode = annotatedNodeOrToken.AsNode().CopyAnnotationsTo(replacementNode)
                            End If
                        Next
 
                        replacementNode = DirectCast(replacementNode, SimpleNameSyntax).WithIdentifier(identifierToken)
                        issueSpan = name.Span
 
                        ' In case the alias name is the same as the last name of the alias target, we only include 
                        ' the left part of the name in the unnecessary span to Not confuse uses.
                        If name.Kind = SyntaxKind.QualifiedName Then
                            Dim qualifiedName As QualifiedNameSyntax = DirectCast(name, QualifiedNameSyntax)
 
                            If qualifiedName.Right.Identifier.ValueText = identifierToken.ValueText Then
                                issueSpan = qualifiedName.Left.Span
                            End If
                        End If
 
                        If CanReplaceWithReducedNameInContext(name, replacementNode) Then
 
                            ' check if the alias name ends with an Attribute suffix that can be omitted.
                            Dim replacementNodeWithoutAttributeSuffix As ExpressionSyntax = Nothing
                            Dim issueSpanWithoutAttributeSuffix As TextSpan = Nothing
                            If TryReduceAttributeSuffix(name, identifierToken, semanticModel, aliasReplacement IsNot Nothing, replacementNodeWithoutAttributeSuffix, issueSpanWithoutAttributeSuffix, cancellationToken) Then
                                If CanReplaceWithReducedName(name, replacementNodeWithoutAttributeSuffix, semanticModel, cancellationToken) Then
                                    replacementNode = replacementNode.CopyAnnotationsTo(replacementNodeWithoutAttributeSuffix)
                                    issueSpan = issueSpanWithoutAttributeSuffix
                                End If
                            End If
 
                            Return True
                        End If
 
                        Return False
                    End If
 
                    Dim nameHasNoAlias = False
 
                    If TypeOf name Is SimpleNameSyntax Then
                        Dim simpleName = DirectCast(name, SimpleNameSyntax)
                        If Not simpleName.Identifier.HasAnnotations(AliasAnnotation.Kind) Then
                            nameHasNoAlias = True
                        End If
                    End If
 
                    If TypeOf name Is QualifiedNameSyntax Then
                        Dim qualifiedNameSyntax = DirectCast(name, QualifiedNameSyntax)
                        If Not qualifiedNameSyntax.Right.Identifier.HasAnnotations(AliasAnnotation.Kind) Then
                            nameHasNoAlias = True
                        End If
                    End If
 
                    Dim aliasInfo = semanticModel.GetAliasInfo(name, cancellationToken)
 
                    ' Don't simplify to predefined type if name is part of a QualifiedName.
                    ' QualifiedNames can't contain PredefinedTypeNames (although MemberAccessExpressions can).
                    ' In other words, the left side of a QualifiedName can't be a PredefinedTypeName.
                    If nameHasNoAlias AndAlso aliasInfo Is Nothing AndAlso
                        Not name.Parent.IsKind(SyntaxKind.QualifiedName) AndAlso
                        Not name.Parent.IsKind(SyntaxKind.NameOfExpression) Then
 
                        Dim type = semanticModel.GetTypeInfo(name, cancellationToken).Type
                        If type IsNot Nothing Then
                            Dim keywordKind = GetPredefinedKeywordKind(type.SpecialType)
                            If keywordKind <> SyntaxKind.None Then
                                ' But do simplify to predefined type if not simplifying results in just the addition of escaping
                                '   brackets.  E.g., even if specified otherwise, prefer `String` to `[String]`.
                                Dim token = SyntaxFactory.Token(
                                    name.GetLeadingTrivia(),
                                    keywordKind,
                                    name.GetTrailingTrivia())
                                Dim valueText = TryCast(name, IdentifierNameSyntax)?.Identifier.ValueText
                                Dim inDeclarationContext = PreferPredefinedTypeKeywordInDeclarations(name, options)
                                Dim inMemberAccessContext = PreferPredefinedTypeKeywordInMemberAccess(name, options)
                                If token.Text = valueText OrElse (inDeclarationContext OrElse inMemberAccessContext) Then
 
                                    Dim codeStyleOptionName As String = Nothing
                                    If inDeclarationContext Then
                                        codeStyleOptionName = NameOf(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration)
                                    ElseIf inMemberAccessContext Then
                                        codeStyleOptionName = NameOf(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess)
                                    End If
 
                                    replacementNode = SyntaxFactory.PredefinedType(token)
                                    issueSpan = name.Span
 
                                    Dim canReplace = CanReplaceWithReducedNameInContext(name, replacementNode)
                                    If canReplace Then
                                        replacementNode = replacementNode.WithAdditionalAnnotations(New SyntaxAnnotation(codeStyleOptionName))
                                    End If
 
                                    Return canReplace
                                End If
                            End If
                        End If
                    End If
 
                    ' Nullable rewrite: Nullable(Of Integer) -> Integer?
                    ' Don't rewrite in the case where Nullable(Of Integer) is part of some qualified name like Nullable(Of Integer).Something
                    If (symbol.Kind = SymbolKind.NamedType) AndAlso (Not name.IsLeftSideOfQualifiedName) Then
                        Dim type = DirectCast(symbol, INamedTypeSymbol)
                        If aliasInfo Is Nothing AndAlso CanSimplifyNullable(type, name) Then
                            Dim genericName As GenericNameSyntax
                            If name.Kind = SyntaxKind.QualifiedName Then
                                genericName = DirectCast(DirectCast(name, QualifiedNameSyntax).Right, GenericNameSyntax)
                            Else
                                genericName = DirectCast(name, GenericNameSyntax)
                            End If
 
                            Dim oldType = genericName.TypeArgumentList.Arguments.First()
                            replacementNode = SyntaxFactory.NullableType(oldType).WithLeadingTrivia(name.GetLeadingTrivia())
 
                            issueSpan = name.Span
 
                            If CanReplaceWithReducedNameInContext(name, replacementNode) Then
                                Return True
                            End If
                        End If
                    End If
                End If
 
                Select Case name.Kind
                    Case SyntaxKind.QualifiedName
                        ' a module name was inserted by the name expansion, so removing this should be tried first.
                        Dim qualifiedName = DirectCast(name, QualifiedNameSyntax)
                        If qualifiedName.HasAnnotation(SimplificationHelpers.SimplifyModuleNameAnnotation) Then
                            If TryOmitModuleName(qualifiedName, semanticModel, replacementNode, issueSpan, cancellationToken) Then
                                Return True
                            End If
                        End If
 
                        replacementNode = qualifiedName.Right.WithLeadingTrivia(name.GetLeadingTrivia())
                        replacementNode = DirectCast(replacementNode, SimpleNameSyntax) _
                            .WithIdentifier(TryEscapeIdentifierToken(DirectCast(replacementNode, SimpleNameSyntax).Identifier))
                        issueSpan = qualifiedName.Left.Span
 
                        If CanReplaceWithReducedName(name, replacementNode, semanticModel, cancellationToken) Then
                            Return True
                        End If
 
                        If TryOmitModuleName(qualifiedName, semanticModel, replacementNode, issueSpan, cancellationToken) Then
                            Return True
                        End If
 
                    Case SyntaxKind.IdentifierName
                        Dim identifier = DirectCast(name, IdentifierNameSyntax).Identifier
                        TryReduceAttributeSuffix(name, identifier, semanticModel, False, replacementNode, issueSpan, cancellationToken)
                End Select
            End If
 
            If replacementNode Is Nothing Then
                Return False
            End If
 
            Return CanReplaceWithReducedName(name, replacementNode, semanticModel, cancellationToken)
        End Function
 
        ''' <summary>
        ''' Checks if the SyntaxNode is a name of a namespace declaration. To be a namespace name, the syntax
        ''' must be parented by an namespace declaration and the node itself must be equal to the declaration's Name
        ''' property.
        ''' </summary>
        Private Shared Function IsPartOfNamespaceDeclarationName(node As SyntaxNode) As Boolean
 
            Dim nextNode As SyntaxNode = node
 
            Do While nextNode IsNot Nothing
 
                Select Case nextNode.Kind
 
                    Case SyntaxKind.IdentifierName, SyntaxKind.QualifiedName
                        node = nextNode
                        nextNode = nextNode.Parent
 
                    Case SyntaxKind.NamespaceStatement
                        Dim namespaceStatement = DirectCast(nextNode, NamespaceStatementSyntax)
                        Return namespaceStatement.Name Is node
 
                    Case Else
                        Return False
 
                End Select
 
            Loop
 
            Return False
        End Function
 
        Private Shared Function CanReplaceWithReducedName(
            name As NameSyntax,
            replacementNode As ExpressionSyntax,
            semanticModel As SemanticModel,
            cancellationToken As CancellationToken
        ) As Boolean
            Dim speculationAnalyzer = New SpeculationAnalyzer(name, replacementNode, semanticModel, cancellationToken)
            If speculationAnalyzer.ReplacementChangesSemantics() Then
                Return False
            End If
 
            Return CanReplaceWithReducedNameInContext(name, replacementNode)
        End Function
 
        Private Shared Function CanReplaceWithReducedNameInContext(name As NameSyntax, replacementNode As ExpressionSyntax) As Boolean
 
            ' Special case.  if this new minimal name parses out to a predefined type, then we
            ' have to make sure that we're not in a using alias.   That's the one place where the
            ' language doesn't allow predefined types.  You have to use the fully qualified name
            ' instead.
            Dim invalidTransformation1 = IsNonNameSyntaxInImportsDirective(name, replacementNode)
            Dim invalidTransformation2 = IsReservedNameInAttribute(name, replacementNode)
 
            Dim invalidTransformation3 = IsNullableTypeSyntaxLeftOfDotInMemberAccess(name, replacementNode)
 
            Return Not (invalidTransformation1 OrElse invalidTransformation2 OrElse invalidTransformation3)
        End Function
 
        Private Shared Function IsNonNameSyntaxInImportsDirective(expression As ExpressionSyntax, simplifiedNode As ExpressionSyntax) As Boolean
            Return TypeOf expression.Parent Is ImportsClauseSyntax AndAlso
                Not TypeOf simplifiedNode Is NameSyntax
        End Function
 
        Private Shared Function IsNullableTypeSyntaxLeftOfDotInMemberAccess(expression As ExpressionSyntax, simplifiedNode As ExpressionSyntax) As Boolean
            Return expression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) AndAlso
                simplifiedNode.Kind = SyntaxKind.NullableType
        End Function
 
        Private Shared Function TryOmitModuleName(name As QualifiedNameSyntax, semanticModel As SemanticModel, <Out()> ByRef replacementNode As ExpressionSyntax, <Out()> ByRef issueSpan As TextSpan, cancellationToken As CancellationToken) As Boolean
            If name.IsParentKind(SyntaxKind.QualifiedName) Then
                Dim symbolForName = semanticModel.GetSymbolInfo(DirectCast(name.Parent, QualifiedNameSyntax), cancellationToken).Symbol
 
                ' in case this QN is used in a "New NSName.ModuleName.MemberName()" expression
                ' the returned symbol is a constructor. Then we need to get the containing type.
                If symbolForName.IsConstructor Then
                    symbolForName = symbolForName.ContainingType
                End If
 
                If symbolForName.IsModuleMember Then
 
                    replacementNode = name.Left.WithLeadingTrivia(name.GetLeadingTrivia())
                    issueSpan = name.Right.Span
 
                    Dim parent = DirectCast(name.Parent, QualifiedNameSyntax)
                    Dim parentReplacement = parent.ReplaceNode(parent.Left, replacementNode)
 
                    If CanReplaceWithReducedName(parent, parentReplacement, semanticModel, cancellationToken) Then
                        Return True
                    End If
                End If
            End If
 
            Return False
        End Function
 
        Private Shared Function TryReduceAttributeSuffix(
            name As NameSyntax,
            identifierToken As SyntaxToken,
            semanticModel As SemanticModel,
            isIdentifierNameFromAlias As Boolean,
            <Out> ByRef replacementNode As ExpressionSyntax,
            <Out> ByRef issueSpan As TextSpan,
            cancellationToken As CancellationToken
        ) As Boolean
            If SyntaxFacts.IsAttributeName(name) AndAlso Not isIdentifierNameFromAlias Then
 
                ' When the replacement is an Alias we don't want the "Attribute" Suffix to be removed because this will result in symbol change
                Dim aliasSymbol = semanticModel.GetAliasInfo(name, cancellationToken)
                If aliasSymbol IsNot Nothing AndAlso
                   String.Compare(aliasSymbol.Name, identifierToken.ValueText, StringComparison.OrdinalIgnoreCase) = 0 Then
                    Return False
                End If
 
                If name.Parent.Kind = SyntaxKind.Attribute OrElse name.IsRightSideOfDot() Then
                    Dim newIdentifierText = String.Empty
 
                    ' an attribute that should keep it (unnecessary "Attribute" suffix should be annotated with a DoNotSimplifyAnnotation
                    If identifierToken.HasAnnotation(SimplificationHelpers.DoNotSimplifyAnnotation) Then
                        newIdentifierText = identifierToken.ValueText + "Attribute"
                    ElseIf identifierToken.ValueText.TryReduceAttributeSuffix(newIdentifierText) Then
                        issueSpan = New TextSpan(name.Span.End - 9, 9)
                    Else
                        Return False
                    End If
 
                    ' escape it (VB allows escaping even for abbreviated identifiers, C# does not!)
                    Dim newIdentifierToken = identifierToken.CopyAnnotationsTo(
                                             SyntaxFactory.Identifier(
                                                 identifierToken.LeadingTrivia,
                                                 newIdentifierText,
                                                 identifierToken.TrailingTrivia))
                    newIdentifierToken = TryEscapeIdentifierToken(newIdentifierToken)
                    replacementNode = SyntaxFactory.IdentifierName(newIdentifierToken).WithLeadingTrivia(name.GetLeadingTrivia())
                    Return True
                End If
            End If
 
            Return False
        End Function
 
        Private Shared Function PreferPredefinedTypeKeywordInDeclarations(name As NameSyntax, options As VisualBasicSimplifierOptions) As Boolean
            Return (Not IsDirectChildOfMemberAccessExpression(name)) AndAlso
                   (Not InsideCrefReference(name)) AndAlso
                   (Not InsideNameOfExpression(name)) AndAlso
                   options.PreferPredefinedTypeKeywordInDeclaration.Value
        End Function
 
        Private Shared Function CanSimplifyNullable(type As INamedTypeSymbol, name As NameSyntax) As Boolean
            If Not type.IsNullable Then
                Return False
            End If
 
            If type.IsUnboundGenericType Then
                ' Don't simplify unbound generic type "Nullable(Of )".
                Return False
            End If
 
            If InsideNameOfExpression(name) Then
                ' Nullable(Of T) can't be simplified to T? in nameof expressions.
                Return False
            End If
 
            If Not InsideCrefReference(name) Then
                ' Nullable(Of T) can always be simplified to T? outside crefs.
                Return True
            End If
 
            ' Inside crefs, if the T in this Nullable(Of T) is being declared right here
            ' then this Nullable(Of T) is not a constructed generic type and we should
            ' not offer to simplify this to T?.
            '
            ' For example, we should not offer the simplification in the following cases where
            ' T does not bind to an existing type / type parameter in the user's code.
            ' - <see cref="Nullable(Of T)"/>
            ' - <see cref="System.Nullable(Of T).Value"/>
            '
            ' And we should offer the simplification in the following cases where SomeType and
            ' SomeMethod bind to a type and method declared elsewhere in the users code.
            ' - <see cref="SomeType.SomeMethod(Nullable(Of SomeType))"/>
 
            If name.IsKind(SyntaxKind.GenericName) Then
                If (name.IsParentKind(SyntaxKind.CrefReference)) OrElse ' cref="Nullable(Of T)"
                   (name.IsParentKind(SyntaxKind.QualifiedName) AndAlso name.Parent?.IsParentKind(SyntaxKind.CrefReference)) OrElse ' cref="System.Nullable(Of T)"
                   (name.IsParentKind(SyntaxKind.QualifiedName) AndAlso If(name.Parent?.IsParentKind(SyntaxKind.QualifiedName), False) AndAlso name.Parent.Parent?.IsParentKind(SyntaxKind.CrefReference)) Then  ' cref="System.Nullable(Of T).Value"
                    ' Unfortunately, unlike in corresponding C# case, we need syntax based checking to detect these cases because of bugs in the VB SemanticModel.
                    ' See https://github.com/dotnet/roslyn/issues/2196, https://github.com/dotnet/roslyn/issues/2197
                    Return False
                End If
            End If
 
            Dim argument = type.TypeArguments.SingleOrDefault()
            If argument Is Nothing OrElse argument.IsErrorType() Then
                Return False
            End If
 
            Dim argumentDecl = argument.DeclaringSyntaxReferences.FirstOrDefault()
            If argumentDecl Is Nothing Then
                ' The type argument is a type from metadata - so this is a constructed generic nullable type that can be simplified (e.g. Nullable(Of Integer)).
                Return True
            End If
 
            Return Not name.Span.Contains(argumentDecl.Span)
        End Function
 
    End Class
End Namespace