File: ExtractMethod\VisualBasicSelectionValidator.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.ExtractMethod
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
 
Namespace Microsoft.CodeAnalysis.VisualBasic.ExtractMethod
    Partial Friend NotInheritable Class VisualBasicExtractMethodService
        Friend NotInheritable Class VisualBasicSelectionValidator
            Inherits SelectionValidator
 
            Public Sub New(document As SemanticDocument, textSpan As TextSpan)
                MyBase.New(document, textSpan)
            End Sub
 
            Protected Overrides Function GetInitialSelectionInfo(cancellationToken As CancellationToken) As InitialSelectionInfo
                Dim root = Me.SemanticDocument.Root
                Dim adjustedSpan = GetAdjustedSpan(Me.OriginalSpan)
                Dim firstTokenInSelection = root.FindTokenOnRightOfPosition(adjustedSpan.Start, includeSkipped:=False)
                Dim lastTokenInSelection = root.FindTokenOnLeftOfPosition(adjustedSpan.End, includeSkipped:=False)
 
                If firstTokenInSelection.Kind = SyntaxKind.None OrElse lastTokenInSelection.Kind = SyntaxKind.None Then
                    Return InitialSelectionInfo.Failure(FeaturesResources.Invalid_selection)
                End If
 
                If firstTokenInSelection <> lastTokenInSelection AndAlso
                   firstTokenInSelection.Span.End > lastTokenInSelection.SpanStart Then
                    Return InitialSelectionInfo.Failure(FeaturesResources.Invalid_selection)
                End If
 
                If (Not adjustedSpan.Contains(firstTokenInSelection.Span)) AndAlso (Not adjustedSpan.Contains(lastTokenInSelection.Span)) Then
                    Return InitialSelectionInfo.Failure(FeaturesResources.Selection_does_not_contain_a_valid_token)
                End If
 
                If (Not firstTokenInSelection.UnderValidContext()) OrElse (Not lastTokenInSelection.UnderValidContext()) Then
                    Return InitialSelectionInfo.Failure(FeaturesResources.No_valid_selection_to_perform_extraction)
                End If
 
                Dim commonRoot = GetCommonRoot(firstTokenInSelection, lastTokenInSelection)
                If commonRoot Is Nothing Then
                    Return InitialSelectionInfo.Failure(FeaturesResources.No_common_root_node_for_extraction)
                End If
 
                If Not commonRoot.ContainedInValidType() Then
                    Return InitialSelectionInfo.Failure(FeaturesResources.Selection_not_contained_inside_a_type)
                End If
 
                Dim selectionInExpression = TypeOf commonRoot Is ExpressionSyntax AndAlso
                                            commonRoot.GetFirstToken(includeZeroWidth:=True) = firstTokenInSelection AndAlso
                                            commonRoot.GetLastToken(includeZeroWidth:=True) = lastTokenInSelection
 
                If (Not selectionInExpression) AndAlso (Not commonRoot.UnderValidContext()) Then
                    Return InitialSelectionInfo.Failure(FeaturesResources.No_valid_selection_to_perform_extraction)
                End If
 
                ' make sure type block enclosing the selection exist
                If commonRoot.GetAncestor(Of TypeBlockSyntax)() Is Nothing Then
                    Return InitialSelectionInfo.Failure(FeaturesResources.No_valid_selection_to_perform_extraction)
                End If
 
                Return CreateInitialSelectionInfo(
                    selectionInExpression, firstTokenInSelection, lastTokenInSelection, cancellationToken)
            End Function
 
            Protected Overrides Function UpdateSelectionInfo(initialSelectionInfo As InitialSelectionInfo, cancellationToken As CancellationToken) As FinalSelectionInfo
                Dim model = Me.SemanticDocument.SemanticModel
 
                Dim selectionInfo = AssignInitialFinalTokens(initialSelectionInfo)
                selectionInfo = AdjustFinalTokensBasedOnContext(selectionInfo, model, cancellationToken)
                selectionInfo = AdjustFinalTokensIfNextStatement(selectionInfo, model, cancellationToken)
                selectionInfo = AssignFinalSpan(initialSelectionInfo, selectionInfo)
                selectionInfo = CheckErrorCasesAndAppendDescriptions(selectionInfo, model, cancellationToken)
 
                Return selectionInfo
            End Function
 
            Protected Overrides Async Function CreateSelectionResultAsync(
                    finalSelectionInfo As FinalSelectionInfo,
                    cancellationToken As CancellationToken) As Task(Of SelectionResult)
 
                Contract.ThrowIfFalse(ContainsValidSelection)
                Contract.ThrowIfFalse(finalSelectionInfo.Status.Succeeded)
 
                Return Await VisualBasicSelectionResult.CreateResultAsync(
                    Me.SemanticDocument, finalSelectionInfo, cancellationToken).ConfigureAwait(False)
            End Function
 
            Private Shared Function CheckErrorCasesAndAppendDescriptions(
                    selectionInfo As FinalSelectionInfo,
                    semanticModel As SemanticModel,
                    cancellationToken As CancellationToken) As FinalSelectionInfo
                If selectionInfo.Status.Failed() Then
                    Return selectionInfo
                End If
 
                Dim clone = selectionInfo
 
                If selectionInfo.FirstTokenInFinalSpan.IsMissing OrElse selectionInfo.LastTokenInFinalSpan.IsMissing Then
                    clone = clone.With(
                        status:=clone.Status.With(succeeded:=False, VBFeaturesResources.contains_invalid_selection))
                End If
 
                ' get the node that covers the selection
                Dim commonNode = GetFinalTokenCommonRoot(selectionInfo)
 
                If selectionInfo.GetSelectionType() <> SelectionType.MultipleStatements AndAlso commonNode.HasDiagnostics() Then
                    clone = clone.With(
                        status:=clone.Status.With(succeeded:=False, VBFeaturesResources.the_selection_contains_syntactic_errors))
                End If
 
                Dim root = semanticModel.SyntaxTree.GetRoot(cancellationToken)
                Dim tokens = root.DescendantTokens(selectionInfo.FinalSpan)
                If tokens.ContainPreprocessorCrossOver(selectionInfo.FinalSpan) Then
                    clone = clone.With(
                        status:=clone.Status.With(succeeded:=True, VBFeaturesResources.Selection_can_t_be_crossed_over_preprocessors))
                End If
 
                ' TODO : check behavior of control flow analysis engine around exception and exception handling.
                If tokens.ContainArgumentlessThrowWithoutEnclosingCatch(selectionInfo.FinalSpan) Then
                    clone = clone.With(
                        status:=clone.Status.With(succeeded:=True, VBFeaturesResources.Selection_can_t_contain_throw_without_enclosing_catch_block))
                End If
 
                If selectionInfo.SelectionInExpression AndAlso commonNode.PartOfConstantInitializerExpression() Then
                    clone = clone.With(
                        status:=clone.Status.With(succeeded:=False, VBFeaturesResources.Selection_can_t_be_parts_of_constant_initializer_expression))
                End If
 
                If selectionInfo.SelectionInExpression AndAlso commonNode.IsArgumentForByRefParameter(semanticModel, cancellationToken) Then
                    clone = clone.With(
                        status:=clone.Status.With(succeeded:=True, VBFeaturesResources.Argument_used_for_ByRef_parameter_can_t_be_extracted_out))
                End If
 
                ' if it is multiple statement case.
                If selectionInfo.GetSelectionType() = SelectionType.MultipleStatements Then
                    If commonNode.GetAncestorOrThis(Of WithBlockSyntax)() IsNot Nothing Then
                        If commonNode.GetImplicitMemberAccessExpressions(selectionInfo.FinalSpan).Any() Then
                            clone = clone.With(
                                status:=clone.Status.With(succeeded:=True, VBFeaturesResources.Implicit_member_access_can_t_be_included_in_the_selection_without_containing_statement))
                        End If
                    End If
 
                    If selectionInfo.FirstTokenInFinalSpan.GetAncestor(Of ExecutableStatementSyntax)() Is Nothing OrElse
                        selectionInfo.LastTokenInFinalSpan.GetAncestor(Of ExecutableStatementSyntax)() Is Nothing Then
                        clone = clone.With(
                            status:=clone.Status.With(succeeded:=False, VBFeaturesResources.Selection_must_be_part_of_executable_statements))
                    End If
                End If
 
                Return clone
            End Function
 
            Private Shared Function GetFinalTokenCommonRoot(selection As FinalSelectionInfo) As SyntaxNode
                Return GetCommonRoot(selection.FirstTokenInFinalSpan, selection.LastTokenInFinalSpan)
            End Function
 
            Private Shared Function GetCommonRoot(token1 As SyntaxToken, token2 As SyntaxToken) As SyntaxNode
                Return token1.GetCommonRoot(token2)
            End Function
 
            Private Shared Function AdjustFinalTokensIfNextStatement(
                    selectionInfo As FinalSelectionInfo,
                    semanticModel As SemanticModel,
                    cancellationToken As CancellationToken) As FinalSelectionInfo
                If selectionInfo.Status.Failed() Then
                    Return selectionInfo
                End If
 
                ' if last statement is next statement, make sure its corresponding loop statement is
                ' included
                Dim nextStatement = selectionInfo.LastTokenInFinalSpan.GetAncestor(Of NextStatementSyntax)()
                If nextStatement Is Nothing OrElse nextStatement.ControlVariables.Count < 2 Then
                    Return selectionInfo
                End If
 
                Dim outmostControlVariable = nextStatement.ControlVariables.Last
 
                Dim symbolInfo = semanticModel.GetSymbolInfo(outmostControlVariable, cancellationToken)
                Dim symbol = symbolInfo.GetBestOrAllSymbols().FirstOrDefault()
 
                ' can't find symbol for the control variable. don't provide extract method
                If symbol Is Nothing OrElse
                   symbol.Locations.Length <> 1 OrElse
                   Not symbol.Locations.First().IsInSource OrElse
                   symbol.Locations.First().SourceTree IsNot semanticModel.SyntaxTree Then
                    Return selectionInfo.With(
                        status:=selectionInfo.Status.With(succeeded:=False, VBFeaturesResources.next_statement_control_variable_doesn_t_have_matching_declaration_statement))
                End If
 
                Dim startPosition = symbol.Locations.First().SourceSpan.Start
                Dim root = semanticModel.SyntaxTree.GetRoot(cancellationToken)
                Dim forBlock = root.FindToken(startPosition).GetAncestor(Of ForOrForEachBlockSyntax)()
                If forBlock Is Nothing Then
                    Return selectionInfo.With(
                        status:=selectionInfo.Status.With(succeeded:=False, VBFeaturesResources.next_statement_control_variable_doesn_t_have_matching_declaration_statement))
                End If
 
                Dim firstStatement = forBlock.ForOrForEachStatement
                Return selectionInfo.With(
                    firstTokenInFinalSpan:=firstStatement.GetFirstToken(includeZeroWidth:=True),
                    lastTokenInFinalSpan:=nextStatement.GetLastToken(includeZeroWidth:=True))
            End Function
 
            Private Shared Function AdjustFinalTokensBasedOnContext(
                    selectionInfo As FinalSelectionInfo,
                    semanticModel As SemanticModel,
                    cancellationToken As CancellationToken) As FinalSelectionInfo
                If selectionInfo.Status.Failed() Then
                    Return selectionInfo
                End If
 
                ' don't need to adjust anything if it is multi-statements case
                If selectionInfo.GetSelectionType() = SelectionType.MultipleStatements Then
                    Return selectionInfo
                End If
 
                ' get the node that covers the selection
                Dim node = GetFinalTokenCommonRoot(selectionInfo)
 
                Dim validNode = Check(semanticModel, node, cancellationToken)
                If validNode Then
                    Return selectionInfo
                End If
 
                Dim firstValidNode = node.GetAncestors(Of SyntaxNode)().FirstOrDefault(
                    Function(n) Check(semanticModel, n, cancellationToken))
 
                If firstValidNode Is Nothing Then
                    ' couldn't find any valid node
                    Return selectionInfo.With(
                        status:=New OperationStatus(succeeded:=False, VBFeaturesResources.Selection_doesn_t_contain_any_valid_node),
                        firstTokenInFinalSpan:=Nothing,
                        lastTokenInFinalSpan:=Nothing)
                End If
 
                Return selectionInfo.With(
                    selectionInExpression:=TypeOf firstValidNode Is ExpressionSyntax,
                    firstTokenInFinalSpan:=firstValidNode.GetFirstToken(includeZeroWidth:=True),
                    lastTokenInFinalSpan:=firstValidNode.GetLastToken(includeZeroWidth:=True))
            End Function
 
            Private Shared Function AssignInitialFinalTokens(
                    selectionInfo As InitialSelectionInfo) As FinalSelectionInfo
 
                If selectionInfo.SelectionInExpression Then
                    ' prefer outer statement or expression if two has same span
                    Dim outerNode = selectionInfo.CommonRoot.GetOutermostNodeWithSameSpan(Function(n) TypeOf n Is ExecutableStatementSyntax OrElse TypeOf n Is ExpressionSyntax)
 
                    ' simple expression case
                    Return New FinalSelectionInfo With {
                        .Status = selectionInfo.Status,
                        .SelectionInExpression = TypeOf outerNode Is ExpressionSyntax,
                        .FirstTokenInFinalSpan = outerNode.GetFirstToken(includeZeroWidth:=True),
                        .LastTokenInFinalSpan = outerNode.GetLastToken(includeZeroWidth:=True)
                        }
                End If
 
                Dim statement1 = selectionInfo.FirstStatement
                Dim statement2 = selectionInfo.LastStatement
 
                If statement1 Is statement2 Then
                    ' check one more time to see whether it is an expression case
                    Dim expression = selectionInfo.CommonRoot.GetAncestor(Of ExpressionSyntax)()
                    If expression IsNot Nothing AndAlso statement1.Span.Contains(expression.Span) Then
                        Return New FinalSelectionInfo With {
                            .Status = selectionInfo.Status,
                            .SelectionInExpression = True,
                            .FirstTokenInFinalSpan = expression.GetFirstToken(includeZeroWidth:=True),
                            .LastTokenInFinalSpan = expression.GetLastToken(includeZeroWidth:=True)
                            }
                    End If
 
                    ' single statement case
                    ' current way to find out a statement that can be extracted out
                    Dim singleStatement = statement1.GetAncestorsOrThis(Of ExecutableStatementSyntax)().FirstOrDefault(
                        Function(s) s.Parent IsNot Nothing AndAlso s.Parent.IsStatementContainerNode() AndAlso s.Parent.ContainStatement(s))
 
                    If singleStatement Is Nothing Then
                        Return New FinalSelectionInfo With {
                            .Status = selectionInfo.Status.With(succeeded:=False, FeaturesResources.No_valid_statement_range_to_extract)
                            }
                    End If
 
                    Return New FinalSelectionInfo With {
                        .Status = selectionInfo.Status,
                        .FirstTokenInFinalSpan = singleStatement.GetFirstToken(includeZeroWidth:=True),
                        .LastTokenInFinalSpan = singleStatement.GetLastToken(includeZeroWidth:=True)
                        }
                End If
 
                ' Special check for vb
                ' either statement1 or statement2 is pointing to header and end of a block node
                ' return the block instead of each node
                If statement1.Parent.IsStatementContainerNode() Then
                    Dim contain1 = statement1.Parent.ContainStatement(statement1)
                    Dim contain2 = statement2.Parent.ContainStatement(statement2)
 
                    If Not contain1 OrElse Not contain2 Then
                        Dim parent = statement1.Parent _
                                               .GetAncestorsOrThis(Of SyntaxNode)() _
                                               .Where(Function(n) TypeOf n Is ExpressionSyntax OrElse TypeOf n Is ExecutableStatementSyntax) _
                                               .First()
 
                        ' single statement case
                        Return New FinalSelectionInfo With {
                            .Status = selectionInfo.Status,
                            .SelectionInExpression = TypeOf parent Is ExpressionSyntax,
                            .FirstTokenInFinalSpan = parent.GetFirstToken(),
                            .LastTokenInFinalSpan = parent.GetLastToken()
                            }
                    End If
                End If
 
                Return New FinalSelectionInfo With {
                    .Status = selectionInfo.Status,
                    .FirstTokenInFinalSpan = statement1.GetFirstToken(includeZeroWidth:=True),
                    .LastTokenInFinalSpan = statement2.GetLastToken(includeZeroWidth:=True)
                    }
            End Function
 
            Protected Overrides Function GetAdjustedSpan(textSpan As TextSpan) As TextSpan
                Dim root = Me.SemanticDocument.Root
                Dim text = Me.SemanticDocument.Text
 
                ' quick exit
                If textSpan.IsEmpty OrElse textSpan.End = 0 Then
                    Return textSpan
                End If
 
                ' regular column 0 check
                Dim line = text.Lines.GetLineFromPosition(textSpan.End)
                If line.Start <> textSpan.End Then
                    Return textSpan
                End If
 
                ' previous line
                Contract.ThrowIfFalse(line.LineNumber > 0)
                Dim previousLine = text.Lines(line.LineNumber - 1)
 
                ' check whether end of previous line is last token of a statement. if it is, don't do anything
                If root.FindTokenOnLeftOfPosition(previousLine.End).IsLastTokenOfStatement() Then
                    Return textSpan
                End If
 
                ' move end position of the selection
                Return textSpan.FromBounds(textSpan.Start, previousLine.End)
            End Function
        End Class
    End Class
End Namespace