File: IntroduceVariable\VisualBasicIntroduceVariableService_IntroduceField.vb
Web Access
Project: src\src\Features\VisualBasic\Portable\Microsoft.CodeAnalysis.VisualBasic.Features.vbproj (Microsoft.CodeAnalysis.VisualBasic.Features)
' 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.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.CodeActions
Imports Microsoft.CodeAnalysis.Formatting
Imports Microsoft.CodeAnalysis.Simplification
Imports Microsoft.CodeAnalysis.VisualBasic
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
 
Namespace Microsoft.CodeAnalysis.VisualBasic.IntroduceVariable
    Partial Friend Class VisualBasicIntroduceVariableService
        Protected Overrides Async Function IntroduceFieldAsync(
                document As SemanticDocument,
                expression As ExpressionSyntax,
                allOccurrences As Boolean,
                isConstant As Boolean,
                cancellationToken As CancellationToken) As Task(Of Document)
 
            Dim oldTypeDeclaration = expression.GetAncestorOrThis(Of TypeBlockSyntax)()
            Dim oldType = If(oldTypeDeclaration IsNot Nothing,
                document.SemanticModel.GetDeclaredSymbol(oldTypeDeclaration.BlockStatement, cancellationToken),
                Nothing)
 
            Dim newNameToken = GenerateUniqueLocalName(
                document, expression, isConstant, containerOpt:=Nothing,
                cancellationToken:=cancellationToken)
 
            Dim newQualifiedName = SyntaxFactory.SimpleMemberAccessExpression(
                expression:=SyntaxFactory.ParseName(oldType.ToNameDisplayString()),
                operatorToken:=SyntaxFactory.Token(SyntaxKind.DotToken),
                name:=SyntaxFactory.IdentifierName(newNameToken)).WithAdditionalAnnotations(Simplifier.Annotation)
 
            If oldType IsNot Nothing Then
                Return Await IntroduceFieldIntoTypeAsync(
                    document, expression, newQualifiedName, oldTypeDeclaration, oldType,
                    newNameToken, allOccurrences, isConstant, cancellationToken).ConfigureAwait(False)
            Else
                Dim oldCompilationUnit = DirectCast(document.Root, CompilationUnitSyntax)
                Dim newCompilationUnit = Rewrite(document, expression, newQualifiedName, document, oldCompilationUnit, allOccurrences, cancellationToken)
                Dim newFieldDeclaration = CreateFieldDeclaration(
                    document, oldTypeDeclaration, newNameToken, expression, allOccurrences, isConstant, cancellationToken)
 
                Dim insertionIndex = If(isConstant, DetermineConstantInsertPosition(oldCompilationUnit.Members, newCompilationUnit.Members), DetermineFieldInsertPosition(oldCompilationUnit.Members, newCompilationUnit.Members))
 
                Dim newRoot = newCompilationUnit.WithMembers(
                    newCompilationUnit.Members.Insert(insertionIndex, newFieldDeclaration))
 
                Return document.Document.WithSyntaxRoot(newRoot)
            End If
        End Function
 
        Private Async Function IntroduceFieldIntoTypeAsync(
                document As SemanticDocument,
                expression As ExpressionSyntax,
                newQualifiedName As MemberAccessExpressionSyntax,
                oldTypeBlock As TypeBlockSyntax,
                oldType As INamedTypeSymbol,
                newNameToken As SyntaxToken,
                allOccurrences As Boolean,
                isConstant As Boolean,
                cancellationToken As CancellationToken) As Task(Of Document)
 
            Dim oldToNewTypeBlockMap = New Dictionary(Of TypeBlockSyntax, TypeBlockSyntax)
 
            For Each declNode In oldType.DeclaringSyntaxReferences.Select(Function(r) r.GetSyntax().Parent).OfType(Of TypeBlockSyntax)()
                Dim currentDocument = Await SemanticDocument.CreateAsync(document.Project.Solution.GetDocument(declNode.SyntaxTree), cancellationToken).ConfigureAwait(False)
                Dim newDeclNode = Rewrite(document, expression, newQualifiedName, currentDocument, declNode, allOccurrences, cancellationToken)
 
                oldToNewTypeBlockMap(declNode) = newDeclNode
            Next
 
            Dim newTypeDeclaration = oldToNewTypeBlockMap(oldTypeBlock)
 
            Dim insertionIndex = GetFieldInsertionIndex(isConstant, oldTypeBlock, newTypeDeclaration, cancellationToken)
            Dim destination As SyntaxNode = oldTypeBlock
 
            Dim newFieldDeclaration = CreateFieldDeclaration(
                document, oldTypeBlock, newNameToken, expression, allOccurrences, isConstant, cancellationToken)
            Dim finalTypeDeclaration = InsertMember(newTypeDeclaration, newFieldDeclaration, insertionIndex)
 
            oldToNewTypeBlockMap(oldTypeBlock) = finalTypeDeclaration
 
            Dim typeBlocksGroupedByTree = oldToNewTypeBlockMap.GroupBy(Function(kvp) kvp.Key.SyntaxTree)
            Dim updatedDocument = document.Document
 
            For Each group In typeBlocksGroupedByTree
                Dim syntaxTree = group.Key
                Dim currentDocument = document.Project.Solution.GetDocument(syntaxTree)
 
                Dim oldRoot = syntaxTree.GetRoot(cancellationToken)
                Dim newRoot = oldRoot.ReplaceNodes(
                    group.Select(Function(kvp) kvp.Key),
                    Function(n1, n2)
                        Return oldToNewTypeBlockMap(n1)
                    End Function)
 
                updatedDocument = updatedDocument.Project.Solution.GetDocument(currentDocument.Id).WithSyntaxRoot(newRoot)
            Next
 
            Return updatedDocument
        End Function
 
        Protected Overrides Function DetermineConstantInsertPosition(oldDeclaration As TypeBlockSyntax, newDeclaration As TypeBlockSyntax) As Integer
            Return DetermineConstantInsertPosition(oldDeclaration.Members, newDeclaration.Members)
        End Function
 
        Protected Overloads Shared Function DetermineConstantInsertPosition(oldMembers As SyntaxList(Of StatementSyntax),
                                                                            newMembers As SyntaxList(Of StatementSyntax)) As Integer
            ' 1) Place the constant after the last constant.
            '
            ' 2) If there is no constant, place it before the first field
            '
            ' 3) If the first change is before either of those, then place before the first
            ' change
            '
            ' 4) Otherwise, place it at the start.
            Dim index = 0
            Dim lastConstantIndex = oldMembers.LastIndexOf(AddressOf IsConstantField)
            If lastConstantIndex >= 0 Then
                index = lastConstantIndex + 1
            Else
                Dim firstFieldIndex = oldMembers.IndexOf(Function(member) TypeOf member Is FieldDeclarationSyntax)
                If firstFieldIndex >= 0 Then
                    index = firstFieldIndex
                End If
 
            End If
 
            Dim firstChangeIndex = DetermineFirstChange(oldMembers, newMembers)
            If firstChangeIndex >= 0 Then
                index = Math.Min(index, firstChangeIndex)
            End If
 
            Return index
        End Function
 
        Protected Overrides Function DetermineFieldInsertPosition(oldDeclaration As TypeBlockSyntax, newDeclaration As TypeBlockSyntax) As Integer
            Return DetermineFieldInsertPosition(oldDeclaration.Members, newDeclaration.Members)
        End Function
 
        Protected Overloads Shared Function DetermineFieldInsertPosition(oldMembers As SyntaxList(Of StatementSyntax),
                                                                         newMembers As SyntaxList(Of StatementSyntax)) As Integer
            ' 1) Place the constant after the last field.
            '
            ' 2) If there is no field, place it after the last constant
            '
            ' 3) If the first change is before either of those, then place before the first
            ' change
            '
            ' 4) Otherwise, place it at the start.
            Dim index = 0
            Dim lastFieldIndex = oldMembers.LastIndexOf(Function(member) TypeOf member Is FieldDeclarationSyntax)
            If lastFieldIndex >= 0 Then
                index = lastFieldIndex + 1
            Else
                Dim lastConstantIndex = oldMembers.LastIndexOf(AddressOf IsConstantField)
                If lastConstantIndex >= 0 Then
                    index = lastConstantIndex + 1
                End If
 
            End If
 
            Dim firstChangeIndex = DetermineFirstChange(oldMembers, newMembers)
            If firstChangeIndex >= 0 Then
                index = Math.Min(index, firstChangeIndex)
            End If
 
            Return index
        End Function
 
        Private Shared Function IsConstantField(member As StatementSyntax) As Boolean
            Dim field = TryCast(member, FieldDeclarationSyntax)
            Return field IsNot Nothing AndAlso field.Modifiers.Any(SyntaxKind.ConstKeyword)
        End Function
 
        Protected Shared Function DetermineFirstChange(oldMembers As SyntaxList(Of StatementSyntax),
                                                       newMembers As SyntaxList(Of StatementSyntax)) As Integer
            Dim i As Integer = 0
 
            While i < oldMembers.Count
                If Not SyntaxFactory.AreEquivalent(oldMembers(i), newMembers(i), topLevel:=False) Then
                    Return i
                End If
 
                i = i + 1
            End While
 
            Return -1
        End Function
 
        Private Function CreateFieldDeclaration(
                document As SemanticDocument,
                oldTypeDeclaration As TypeBlockSyntax,
                newNameToken As SyntaxToken,
                expression As ExpressionSyntax,
                allOccurrences As Boolean,
                isConstant As Boolean,
                cancellationToken As CancellationToken) As FieldDeclarationSyntax
 
            Dim matches = FindMatches(document, expression, document, oldTypeDeclaration, allOccurrences, cancellationToken)
 
            Dim trimmedExpression = expression.WithoutTrailingTrivia().WithoutLeadingTrivia()
            Return SyntaxFactory.FieldDeclaration(
                    Nothing,
                    MakeFieldModifiers(matches, isConstant, inScript:=oldTypeDeclaration Is Nothing, inModule:=oldTypeDeclaration.Kind = SyntaxKind.ModuleBlock),
                    SyntaxFactory.SingletonSeparatedList(
                        SyntaxFactory.VariableDeclarator(
                            SyntaxFactory.SingletonSeparatedList(SyntaxFactory.ModifiedIdentifier(newNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()))),
                            asClause:=SyntaxFactory.SimpleAsClause(GetTypeSymbol(document, expression, cancellationToken).GenerateTypeSyntax()),
                            initializer:=SyntaxFactory.EqualsValue(value:=trimmedExpression)))) _
                    .WithAppendedTrailingTrivia(SyntaxFactory.ElasticMarker) _
                    .WithAdditionalAnnotations(Formatter.Annotation)
 
        End Function
 
        Private Shared Function MakeFieldModifiers(expressions As IEnumerable(Of ExpressionSyntax),
                                            isConstant As Boolean,
                                            inScript As Boolean,
                                            inModule As Boolean) As SyntaxTokenList
            Dim modifiers = New List(Of SyntaxToken)
 
            Dim inTypeAttribute = expressions.Select(Function(e) e.GetAncestor(Of AttributeListSyntax)()).
                                              WhereNotNull().
                                              Any(Function(a) TypeOf a.Parent Is TypeStatementSyntax)
 
            If inTypeAttribute Then
                modifiers.Add(SyntaxFactory.Token(SyntaxKind.FriendKeyword))
            Else
                modifiers.Add(SyntaxFactory.Token(SyntaxKind.PrivateKeyword))
            End If
 
            If isConstant Then
                modifiers.Add(SyntaxFactory.Token(SyntaxKind.ConstKeyword))
            ElseIf inScript Then
                modifiers.Add(SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))
            ElseIf inModule Then
                modifiers.Add(SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))
            Else
                modifiers.Add(SyntaxFactory.Token(SyntaxKind.SharedKeyword))
                modifiers.Add(SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))
            End If
 
            Return SyntaxFactory.TokenList(modifiers)
        End Function
 
        Protected Shared Function InsertMember(typeDeclaration As TypeBlockSyntax,
                                               memberDeclaration As StatementSyntax,
                                               index As Integer) As TypeBlockSyntax
            Return typeDeclaration.WithMembers(
                typeDeclaration.Members.Insert(index, memberDeclaration))
        End Function
    End Class
End Namespace