File: ConvertForEachToFor\VisualBasicConvertForEachToForCodeRefactoringProvider.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.Composition
Imports System.Diagnostics.CodeAnalysis
Imports System.Threading
Imports Microsoft.CodeAnalysis.CodeActions
Imports Microsoft.CodeAnalysis.CodeRefactorings
Imports Microsoft.CodeAnalysis.ConvertForEachToFor
Imports Microsoft.CodeAnalysis.Editing
Imports Microsoft.CodeAnalysis.Operations
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
 
Namespace Microsoft.CodeAnalysis.VisualBasic.ConvertForEachToFor
    <ExportCodeRefactoringProvider(LanguageNames.VisualBasic, Name:=PredefinedCodeRefactoringProviderNames.ConvertForEachToFor), [Shared]>
    Friend Class VisualBasicConvertForEachToForCodeRefactoringProvider
        Inherits AbstractConvertForEachToForCodeRefactoringProvider(Of StatementSyntax, ForEachBlockSyntax)
 
        <ImportingConstructor>
        <SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification:="Used in test code: https://github.com/dotnet/roslyn/issues/42814")>
        Public Sub New()
        End Sub
 
        Protected Overrides ReadOnly Property Title As String = VBFeaturesResources.Convert_to_For
 
        Protected Overrides Function IsValid(foreachNode As ForEachBlockSyntax) As Boolean
            ' we don't support colon separated statements
            Return Not foreachNode.DescendantTrivia().Any(Function(t) t.IsKind(SyntaxKind.ColonTrivia))
        End Function
 
        Protected Overrides Function ValidLocation(foreachInfo As ForEachInfo) As Boolean
            ' all places where for each can appear is valid location for vb
            Return True
        End Function
 
        Protected Overrides Function GetForEachBody(foreachBlock As ForEachBlockSyntax) As (start As SyntaxNode, [end] As SyntaxNode)
            If foreachBlock.Statements.Count = 0 Then
                Return Nothing
            End If
 
            Return (foreachBlock.Statements(0), foreachBlock.Statements(foreachBlock.Statements.Count - 1))
        End Function
 
        Protected Overrides Sub ConvertToForStatement(model As SemanticModel, foreachInfo As ForEachInfo, editor As SyntaxEditor, cancellationToken As CancellationToken)
            cancellationToken.ThrowIfCancellationRequested()
 
            Dim generator = editor.Generator
            Dim forEachBlock = foreachInfo.ForEachStatement
 
            ' trailing triva of expression will be attached to for statement below
            Dim foreachCollectionExpression = forEachBlock.ForEachStatement.Expression
            Dim collectionVariable = GetCollectionVariableName(
                model, generator, foreachInfo, foreachCollectionExpression, cancellationToken)
 
            ' make sure we get rid of all comments from expression since that will be re-attached to for statement
            Dim expression = foreachCollectionExpression.WithTrailingTrivia(
                    foreachCollectionExpression.GetTrailingTrivia().Where(Function(t) t.IsWhitespaceOrEndOfLine()))
 
            ' and remove all trailing trivia if it is used for cast
            If foreachInfo.ExplicitCastInterface IsNot Nothing Then
                expression = expression.WithoutTrailingTrivia()
            End If
 
            ' first, see whether we need to introduce New statement to capture collection
            IntroduceCollectionStatement(foreachInfo, editor, type:=Nothing, expression, collectionVariable)
 
            ' create New index variable name
            Dim indexVariable = If(
                forEachBlock.Statements.Count = 0,
                generator.Identifier("i"),
                CreateUniqueName(foreachInfo.SemanticFacts, model, forEachBlock.Statements(0), "i", cancellationToken))
 
            ' put variable statement in body
            Dim bodyStatement = GetForLoopBody(generator, foreachInfo, collectionVariable, indexVariable)
 
            Dim nextStatement = forEachBlock.NextStatement
 
            If nextStatement.ControlVariables.Count > 0 Then
                Debug.Assert(nextStatement.ControlVariables.Count = 1)
 
                Dim controlVariable As ExpressionSyntax = nextStatement.ControlVariables(0)
                controlVariable = CType(generator.IdentifierName(
                    indexVariable _
                        .WithLeadingTrivia(controlVariable.GetFirstToken().LeadingTrivia) _
                        .WithTrailingTrivia(controlVariable.GetLastToken().TrailingTrivia)), ExpressionSyntax)
 
                nextStatement = nextStatement.WithControlVariables(
                    SyntaxFactory.SingletonSeparatedList(controlVariable))
            End If
 
            ' create for statement from foreach statement
            Dim forBlock = SyntaxFactory.ForBlock(
                SyntaxFactory.ForStatement(
                    DirectCast(generator.IdentifierName(indexVariable.WithAdditionalAnnotations(RenameAnnotation.Create())), VisualBasicSyntaxNode),
                    DirectCast(generator.LiteralExpression(0), ExpressionSyntax),
                    DirectCast(generator.SubtractExpression(
                        generator.MemberAccessExpression(
                            collectionVariable, foreachInfo.CountName), generator.LiteralExpression(1)), ExpressionSyntax)),
                bodyStatement,
                nextStatement)
 
            If foreachInfo.RequireCollectionStatement Then
                ' this is to remove blank line between newly added collection statement with "For" keyword.
                ' default VB formatting rule around elastic trivia and end of line trivia is converting elastic trivia
                ' to end of line trivia causing there to be 2 line breaks. this fix that issue. not changing the default
                ' rule since it will affect other ones that having opposite desire.
                forBlock = forBlock.WithLeadingTrivia(SyntaxFactory.TriviaList())
            Else
                ' transfer comment on "For Each" to "For"
                forBlock = forBlock.WithLeadingTrivia(forEachBlock.GetLeadingTrivia())
            End If
 
            ' transfer comment at then end of "For Each" to "For"
            forBlock = forBlock.WithForStatement(forBlock.ForStatement.WithTrailingTrivia(forEachBlock.ForEachStatement.GetLastToken().TrailingTrivia))
 
            editor.ReplaceNode(forEachBlock, forBlock)
        End Sub
 
        Private Function GetForLoopBody(
            generator As SyntaxGenerator, foreachInfo As ForEachInfo,
            collectionVariableName As SyntaxNode, indexVariable As SyntaxToken) As SyntaxList(Of StatementSyntax)
 
            Dim forEachBlock = foreachInfo.ForEachStatement
 
            Dim foreachVariable As SyntaxNode = Nothing
            Dim type As SyntaxNode = Nothing
            GetVariableNameAndType(forEachBlock.ForEachStatement, foreachVariable, type)
 
            ' use original text
            Dim foreachVariableToken = generator.Identifier(foreachVariable.ToString())
 
            ' create variable statement
            Dim variableStatement = AddItemVariableDeclaration(
                generator, type, foreachVariableToken, foreachInfo.ForEachElementType, collectionVariableName, indexVariable)
 
            If forEachBlock.Statements.Count = 0 Then
                ' If the block was empty, still put the new variable inside of it. This handles the case where the user
                ' writes the foreach and immediately decides to change it to a for-loop.  Now they'll still have their
                ' variable to use in the body instead of having to write it again.
                Return SyntaxFactory.SingletonList(variableStatement)
            End If
 
            ' Nested loops might not have a Next statement
            If IsForEachVariableWrittenInside Then
                variableStatement = variableStatement.WithAdditionalAnnotations(CreateWarningAnnotation())
            End If
 
            Return forEachBlock.Statements.Insert(0, DirectCast(variableStatement, StatementSyntax))
        End Function
 
        Private Shared Sub GetVariableNameAndType(
            forEachStatement As ForEachStatementSyntax, ByRef foreachVariable As SyntaxNode, ByRef type As SyntaxNode)
 
            Dim controlVariable = forEachStatement.ControlVariable
 
            Dim declarator = TryCast(controlVariable, VariableDeclaratorSyntax)
            If declarator IsNot Nothing Then
                foreachVariable = declarator.Names(0)
                type = declarator.AsClause.Type
            Else
                foreachVariable = controlVariable
                type = Nothing
            End If
        End Sub
 
        Protected Overrides Function IsSupported(foreachVariable As ILocalSymbol, foreachOperation As IForEachLoopOperation, foreachStatement As ForEachBlockSyntax) As Boolean
            ' VB can have Next variable. but we only support
            ' simple 1 variable case.
            If foreachOperation.NextVariables.Length > 1 Then
                Return False
            End If
 
            If foreachOperation.NextVariables.IsEmpty AndAlso foreachStatement.NextStatement Is Nothing Then
                Return False
            End If
 
            ' It is okay to omit variable in next, but if it presents, it must be same as one in the loop
            If Not foreachOperation.NextVariables.IsEmpty Then
                Dim nextVariable = TryCast(foreachOperation.NextVariables(0), ILocalReferenceOperation)
                If nextVariable Is Nothing OrElse nextVariable.Local?.Equals(foreachVariable) = False Then
                    ' We do not support anything else than local reference for next variable
                    ' operation
                    Return False
                End If
            End If
 
            Return True
        End Function
    End Class
End Namespace