File: CodeCleanup\Providers\RemoveUnnecessaryLineContinuationCodeCleanupProvider.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.Collections.Immutable
Imports System.Composition
Imports System.Diagnostics.CodeAnalysis
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.Formatting
Imports Microsoft.CodeAnalysis.Host
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
 
Namespace Microsoft.CodeAnalysis.CodeCleanup.Providers
    <ExportCodeCleanupProvider(PredefinedCodeCleanupProviderNames.RemoveUnnecessaryLineContinuation, LanguageNames.VisualBasic), [Shared]>
    <ExtensionOrder(After:=PredefinedCodeCleanupProviderNames.NormalizeModifiersOrOperators, Before:=PredefinedCodeCleanupProviderNames.Format)>
    Friend Class RemoveUnnecessaryLineContinuationCodeCleanupProvider
        Implements ICodeCleanupProvider
 
        <ImportingConstructor>
        <SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification:="https://github.com/dotnet/roslyn/issues/42820")>
        Public Sub New()
        End Sub
 
        Public ReadOnly Property Name As String Implements ICodeCleanupProvider.Name
            Get
                Return PredefinedCodeCleanupProviderNames.RemoveUnnecessaryLineContinuation
            End Get
        End Property
 
        Public Async Function CleanupAsync(document As Document, spans As ImmutableArray(Of TextSpan), options As CodeCleanupOptions, cancellationToken As CancellationToken) As Task(Of Document) Implements ICodeCleanupProvider.CleanupAsync
            ' Is this VB 9? If so, we shouldn't remove line continuations because implicit line continuation was introduced in VB 10.
            Dim parseOptions = TryCast(document.Project.ParseOptions, VisualBasicParseOptions)
            If parseOptions?.LanguageVersion <= LanguageVersion.VisualBasic9 Then
                Return document
            End If
 
            Dim root = Await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(False)
            Dim newRoot = Await CleanupAsync(root, spans, options.FormattingOptions, document.Project.Solution.Services, cancellationToken).ConfigureAwait(False)
 
            Return If(newRoot Is root, document, document.WithSyntaxRoot(newRoot))
        End Function
 
        Public Function CleanupAsync(root As SyntaxNode, spans As ImmutableArray(Of TextSpan), options As SyntaxFormattingOptions, services As SolutionServices, cancellationToken As CancellationToken) As Task(Of SyntaxNode) Implements ICodeCleanupProvider.CleanupAsync
            Return Task.FromResult(Replacer.Process(root, spans, cancellationToken))
        End Function
 
        Private Class Replacer
            Private ReadOnly _leading As New Dictionary(Of SyntaxToken, SyntaxTriviaList)
            Private ReadOnly _trailing As New Dictionary(Of SyntaxToken, SyntaxTriviaList)
            Private ReadOnly _tokens As New Dictionary(Of SyntaxToken, SyntaxToken)
 
            Private ReadOnly _root As SyntaxNode
            Private ReadOnly _spans As ImmutableArray(Of TextSpan)
 
            Public Shared Function Process(root As SyntaxNode, spans As ImmutableArray(Of TextSpan), cancellationToken As CancellationToken) As SyntaxNode
                Dim replacer = New Replacer(root, spans)
                Return replacer.Do(cancellationToken)
            End Function
 
            Private Sub New(root As SyntaxNode, spans As ImmutableArray(Of TextSpan))
                _root = root
                _spans = spans
            End Sub
 
            Private Function [Do](cancellationToken As CancellationToken) As SyntaxNode
                _spans.Do(Sub(s) Cleanup(_root, s, cancellationToken))
 
                If _leading.Count = 0 AndAlso _trailing.Count = 0 AndAlso _tokens.Count = 0 Then
                    Return _root
                End If
 
                Dim newRoot = _root.ReplaceTokens(_leading.Keys.Concat(_trailing.Keys).Concat(_tokens.Keys).Distinct(),
                                                 Function(n, m)
                                                     Dim token = n
 
                                                     ' replace token if needed
                                                     Dim remappedToken As SyntaxToken = Nothing
                                                     If _tokens.TryGetValue(token, remappedToken) Then
                                                         token = remappedToken
                                                     End If
 
                                                     ' replace leading trivia if needed
                                                     Dim current = token
                                                     Dim triviaList As SyntaxTriviaList = Nothing
                                                     If _leading.TryGetValue(token, triviaList) Then
                                                         current = current.WithLeadingTrivia(triviaList)
                                                     End If
 
                                                     ' replace trailing trivia if needed
                                                     If _trailing.TryGetValue(token, triviaList) Then
                                                         current = current.WithTrailingTrivia(triviaList)
                                                     End If
 
                                                     Return current
                                                 End Function)
 
                Return newRoot
            End Function
 
            Private Sub Cleanup(root As SyntaxNode, span As TextSpan, cancellationToken As CancellationToken)
                cancellationToken.ThrowIfCancellationRequested()
 
                ' go through all token in pair
                Dim token1 As SyntaxToken = Nothing
                For Each token2 In root.DescendantTokens(span)
                    cancellationToken.ThrowIfCancellationRequested()
 
                    ProcessAroundColon(token1, token2)
                    ProcessExplicitLineContinuation(token1, token2)
 
                    ' hold on to previous and one before previous token
                    token1 = token2
                Next
            End Sub
 
            Private Sub ProcessExplicitLineContinuation(token1 As SyntaxToken, token2 As SyntaxToken)
                ' we are at the very beginning
                If token1.Kind = SyntaxKind.None Then
                    Return
                End If
 
                ' check context
                If token1.IsLastTokenOfStatement() AndAlso Not token1.IsMissing Then
                    ' check trivia
                    If Not GetTrailingTrivia(token1).Any(SyntaxKind.LineContinuationTrivia) AndAlso
                       Not GetLeadingTrivia(token2).Any(SyntaxKind.LineContinuationTrivia) Then
                        Return
                    End If
 
                    ' Do not remove line continuations within single line constructs.
                    If PartOfSinglelineConstruct(token1) Then
                        Return
                    End If
 
                    ReplaceLineContinuationToEndOfLine(token1, token2)
                    Return
                End If
 
                If SyntaxFacts.AllowsTrailingImplicitLineContinuation(token1) OrElse
                   SyntaxFacts.AllowsLeadingImplicitLineContinuation(token2) Then
                    If LineDelta(token1, token2) > 1 Then
                        Return
                    End If
 
                    ' check trivia
                    If Not GetTrailingTrivia(token1).Any(SyntaxKind.LineContinuationTrivia) AndAlso
                       Not GetLeadingTrivia(token2).Any(SyntaxKind.LineContinuationTrivia) Then
                        Return
                    End If
 
                    ReplaceLineContinuationToEndOfLine(token1, token2)
                    Return
                End If
            End Sub
 
            Private Sub ProcessAroundColon(token1 As SyntaxToken, token2 As SyntaxToken)
                ' remove colon trivia that are not needed
                Dim colonInTrailing = GetTrailingTrivia(token1).Any(SyntaxKind.ColonTrivia)
                Dim colonInLeading = GetLeadingTrivia(token2).Any(SyntaxKind.ColonTrivia)
 
                If Not colonInLeading AndAlso Not colonInTrailing Then
                    Return
                End If
 
                ' trivia can have multiple consecutive colons, make sure we normalize them to one colon
                ReplaceTrailingTrivia(token1, RemoveTrailingColonTrivia(token1, RemoveConsecutiveColons(GetTrailingTrivia(token1))).ToSyntaxTriviaList())
                ReplaceLeadingTrivia(token2, RemoveConsecutiveColons(GetLeadingTrivia(token2)).ToSyntaxTriviaList())
 
                ' bug # 12899
                ' never delete colon token that belongs to label
                If IsLabelToken(token1) Then
                    RemoveColonAfterLabel(token1, token2)
                    Return
                End If
 
                ' check whether token2 and token3 is on same line
                Dim trailingTrivia = GetTrailingTrivia(token1)
                Dim leadingTrivia = GetLeadingTrivia(token2)
 
                ' colon and next token must not be on same line
                If OnSimpleLine(token1, token2) Then
                    Return
                End If
 
                ' if colon contains any skipped token, don't do anything
                If leadingTrivia.Any(SyntaxKind.SkippedTokensTrivia) Then
                    Return
                End If
 
                Dim token2Kind = GetToken(token2).Kind
                Dim dotOrExclamationInWithBlock =
                    (token2Kind = SyntaxKind.DotToken OrElse
                      token2Kind = SyntaxKind.ExclamationToken) AndAlso
                    (token2.GetAncestor(Of WithBlockSyntax)() IsNot Nothing OrElse
                     token2.GetAncestor(Of ObjectMemberInitializerSyntax)() IsNot Nothing)
 
                ' don't remove colon in these cases:
 
                ' (1)
                ' With ""
                '   Dim y = From x In "" Distinct :
                '   .ToLower()
                ' End With
 
                ' (2)
                ' With ""
                '   Dim y = From x In "" Distinct :
                '       !A = !B
                ' End With
 
                If GetToken(token1).Kind = SyntaxKind.DistinctKeyword AndAlso dotOrExclamationInWithBlock Then
                    Return
                End If
 
                ' check whether colon is for statements inside of single-line statement
                If (colonInTrailing AndAlso PartOfSinglelineConstruct(token1)) OrElse
                   (colonInLeading AndAlso PartOfSinglelineConstruct(token2)) Then
                    Return
                End If
 
                ' colon is not on the same line, remove colon trivia
                ReplaceTrailingTrivia(token1, trailingTrivia.Where(Function(t) t.Kind <> SyntaxKind.ColonTrivia).ToSyntaxTriviaList())
 
                If colonInLeading And dotOrExclamationInWithBlock Then
                    Return
                End If
 
                ReplaceLeadingTrivia(token2, leadingTrivia.Where(Function(t) t.Kind <> SyntaxKind.ColonTrivia).ToSyntaxTriviaList())
            End Sub
 
            Private Shared Function RemoveTrailingColonTrivia(token1 As SyntaxToken, trailing As IEnumerable(Of SyntaxTrivia)) As IEnumerable(Of SyntaxTrivia)
                If token1.Kind <> SyntaxKind.ColonToken OrElse trailing.Count = 0 Then
                    Return trailing
                End If
 
                If trailing(0).Kind = SyntaxKind.ColonTrivia Then
                    Return trailing.SkipWhile(Function(t) t.Kind = SyntaxKind.ColonTrivia)
                End If
 
                Return trailing
            End Function
 
            Private Function OnSimpleLine(token1 As SyntaxToken, token2 As SyntaxToken) As Boolean
                ' colon and next token must not be on same line
                Return LineDelta(token1, token2) = 0
            End Function
 
            Private Function LineDelta(token1 As SyntaxToken, token2 As SyntaxToken) As Integer
                Return GetTrailingTrivia(token1).ToFullString().GetNumberOfLineBreaks() +
                       GetLeadingTrivia(token2).ToFullString().GetNumberOfLineBreaks()
            End Function
 
            Private Shared Function IsLabelToken(token As SyntaxToken) As Boolean
                Return TypeOf token.Parent Is LabelStatementSyntax
            End Function
 
            Private Shared Function PartOfSinglelineConstruct(token As SyntaxToken) As Boolean
                Dim node = token.Parent
                While node IsNot Nothing
                    If TypeOf node Is SingleLineIfStatementSyntax OrElse
                       TypeOf node Is SingleLineLambdaExpressionSyntax Then
                        Return True
                    End If
 
                    node = node.Parent
                End While
 
                Return False
            End Function
 
            Private Shared Iterator Function RemoveConsecutiveColons(trivia As SyntaxTriviaList) As IEnumerable(Of SyntaxTrivia)
                Dim last As SyntaxTrivia = Nothing
                For Each t In trivia
                    If t.Kind <> SyntaxKind.ColonTrivia OrElse
                        last.Kind <> SyntaxKind.ColonTrivia Then
                        Yield t
                    End If
 
                    last = t
                Next
            End Function
 
            Private Sub RemoveColonAfterLabel(token1 As SyntaxToken, token2 As SyntaxToken)
                Dim colon = False
 
                Dim trailing = New List(Of SyntaxTrivia)
                Dim leading = New List(Of SyntaxTrivia)
 
                For Each trivia In GetTrailingTrivia(token1)
                    If trivia.Kind = SyntaxKind.ColonTrivia Then
                        If colon Then
                            Continue For
                        End If
 
                        colon = True
                    End If
 
                    trailing.Add(trivia)
                Next
 
                For Each trivia In GetLeadingTrivia(token2)
                    If trivia.Kind = SyntaxKind.ColonTrivia Then
                        If colon Then
                            Continue For
                        End If
 
                        colon = True
                    End If
 
                    leading.Add(trivia)
                Next
 
                ReplaceTrailingTrivia(token1, trailing.ToSyntaxTriviaList())
                ReplaceLeadingTrivia(token2, leading.ToSyntaxTriviaList())
            End Sub
 
            Private Sub ReplaceLineContinuationToEndOfLine(token1 As SyntaxToken, token2 As SyntaxToken)
                ' only whitespace and line continuation is valid. otherwise, we don't touch
                Dim trailingTrivia = GetTrailingTrivia(token1)
                Dim leadingTrivia = GetLeadingTrivia(token2)
                If ContainsInapplicableTrivia(trailingTrivia) OrElse ContainsInapplicableTrivia(leadingTrivia) Then
                    Return
                End If
 
                ReplaceTrailingTrivia(token1, ReplaceLineContinuationToEndOfLine(trailingTrivia).ToSyntaxTriviaList())
                ReplaceLeadingTrivia(token2, ReplaceLineContinuationToEndOfLine(leadingTrivia).ToSyntaxTriviaList())
            End Sub
 
            Private Shared Function ContainsInapplicableTrivia(trivia As SyntaxTriviaList) As Boolean
                Return trivia.Any(Function(t) t.Kind <> SyntaxKind.WhitespaceTrivia AndAlso
                                              t.Kind <> SyntaxKind.LineContinuationTrivia AndAlso
                                              t.Kind <> SyntaxKind.EndOfLineTrivia)
            End Function
 
            Private Shared Function ReplaceLineContinuationToEndOfLine(trivia As IEnumerable(Of SyntaxTrivia)) As IEnumerable(Of SyntaxTrivia)
                Return trivia.Where(Function(t) t.Kind <> SyntaxKind.LineContinuationTrivia)
            End Function
 
            Private Function GetLeadingTrivia(token As SyntaxToken) As SyntaxTriviaList
                Return GetTriviaList(token, _leading, token.LeadingTrivia)
            End Function
 
            Private Function GetTrailingTrivia(token As SyntaxToken) As SyntaxTriviaList
                Return GetTriviaList(token, _trailing, token.TrailingTrivia)
            End Function
 
            Private Function GetTriviaList(token As SyntaxToken,
                                           map As Dictionary(Of SyntaxToken, SyntaxTriviaList),
                                           defaultTrivia As SyntaxTriviaList) As SyntaxTriviaList
                Dim value As SyntaxTriviaList = Nothing
                If map.TryGetValue(GetToken(token), value) Then
                    Return value
                End If
 
                Return defaultTrivia
            End Function
 
            Private Sub ReplaceTrailingTrivia(token As SyntaxToken, trivia As SyntaxTriviaList)
                ReplaceTrivia(token, _trailing, trivia)
            End Sub
 
            Private Sub ReplaceLeadingTrivia(token As SyntaxToken, trivia As SyntaxTriviaList)
                ReplaceTrivia(token, _leading, trivia)
            End Sub
 
            Private Sub ReplaceTrivia(token As SyntaxToken,
                                      map As Dictionary(Of SyntaxToken, SyntaxTriviaList),
                                      trivia As SyntaxTriviaList)
                map(GetToken(token)) = trivia
            End Sub
 
            Private Function GetToken(token As SyntaxToken) As SyntaxToken
                Dim value As SyntaxToken = Nothing
                If _tokens.TryGetValue(token, value) Then
                    Return value
                End If
 
                Return token
            End Function
        End Class
    End Class
End Namespace