File: Parser\ParseInterpolatedString.vb
Web Access
Project: src\roslyn\src\Compilers\VisualBasic\Portable\Microsoft.CodeAnalysis.VisualBasic.vbproj (Microsoft.CodeAnalysis.VisualBasic)
' 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 Microsoft.CodeAnalysis.Syntax.InternalSyntax
Imports InternalSyntaxFactory = Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax.SyntaxFactory

'
'============ Methods for parsing portions of executable statements ==
'

Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax

    Partial Friend Class Parser

        Private Function ParseInterpolatedStringExpression() As InterpolatedStringExpressionSyntax
            Debug.Assert(CurrentToken.Kind = SyntaxKind.DollarSignDoubleQuoteToken, "ParseInterpolatedStringExpression called on the wrong token.")

            ResetCurrentToken(ScannerState.InterpolatedStringPunctuation)

            Debug.Assert(CurrentToken.Kind = SyntaxKind.DollarSignDoubleQuoteToken, "Rescanning $"" failed.")

            Dim dollarSignDoubleQuoteToken = DirectCast(CurrentToken, PunctuationSyntax)
            GetNextToken(ScannerState.InterpolatedStringContent)

            Dim contentBuilder = _pool.Allocate(Of InterpolatedStringContentSyntax)

            Dim doubleQuoteToken As PunctuationSyntax
            Dim skipped As SyntaxListBuilder(Of SyntaxToken) = Nothing
            Do
                Dim content As InterpolatedStringContentSyntax

                Select Case CurrentToken.Kind
                    Case SyntaxKind.InterpolatedStringTextToken

                        Dim textToken = DirectCast(CurrentToken, InterpolatedStringTextTokenSyntax)

                        ' At this point we're either right before an {, an } (error), or a "".
                        GetNextToken(ScannerState.InterpolatedStringPunctuation)

                        Debug.Assert(CurrentToken.Kind <> SyntaxKind.InterpolatedStringTextToken,
                                     "Two interpolated string text literal tokens encountered back-to-back. " &
                                     "Scanner should have scanned these as a single token.")

                        content = SyntaxFactory.InterpolatedStringText(textToken)

                    Case SyntaxKind.OpenBraceToken

                        content = ParseInterpolatedStringInterpolation()

                    Case SyntaxKind.CloseBraceToken

                        If skipped.IsNull Then
                            skipped = _pool.Allocate(Of SyntaxToken)
                        End If

                        skipped.Add(CurrentToken)
                        GetNextToken(ScannerState.InterpolatedStringContent)
                        Continue Do

                    Case SyntaxKind.DoubleQuoteToken

                        doubleQuoteToken = DirectCast(CurrentToken, PunctuationSyntax)
                        GetNextToken()
                        Exit Do

                    Case SyntaxKind.EndOfInterpolatedStringToken

                        doubleQuoteToken = InternalSyntaxFactory.MissingPunctuation(SyntaxKind.DoubleQuoteToken)
                        GetNextToken(ScannerState.VB)
                        Exit Do

                    Case Else

                        doubleQuoteToken = InternalSyntaxFactory.MissingPunctuation(SyntaxKind.DoubleQuoteToken)
                        Exit Do

                End Select

                If Not skipped.IsNull Then
                    content = AddLeadingSyntax(content, _pool.ToListAndFree(skipped), ERRID.ERR_Syntax)
                    skipped = Nothing
                End If

                contentBuilder.Add(content)
            Loop

            If Not skipped.IsNull Then
                doubleQuoteToken = AddLeadingSyntax(doubleQuoteToken, _pool.ToListAndFree(skipped), ERRID.ERR_Syntax)
                skipped = Nothing
            End If

            Dim node = SyntaxFactory.InterpolatedStringExpression(dollarSignDoubleQuoteToken,
                                                              _pool.ToListAndFree(contentBuilder),
                                                              doubleQuoteToken)
            Return CheckFeatureAvailability(Feature.InterpolatedStrings, node)
        End Function

        Private Function ParseInterpolatedStringInterpolation() As InterpolationSyntax
            Debug.Assert(CurrentToken.Kind = SyntaxKind.OpenBraceToken, "ParseInterpolatedStringEmbeddedExpression called on the wrong token.")

            Dim colonToken As PunctuationSyntax = Nothing
            Dim excessText As String = Nothing

            Dim openBraceToken = DirectCast(CurrentToken, PunctuationSyntax)
            GetNextToken(ScannerState.VB)

            Dim expression As ExpressionSyntax

            If CurrentToken.Kind = SyntaxKind.ColonToken Then

                openBraceToken = DirectCast(RemoveTrailingColonTriviaAndConvertToColonToken(openBraceToken, colonToken, excessText), PunctuationSyntax)
                expression = ReportSyntaxError(InternalSyntaxFactory.MissingExpression(), ERRID.ERR_ExpectedExpression)

            Else
                expression = ParseExpressionCore()

                ' Scanned this as a terminator. Fix it.
                If CurrentToken.Kind = SyntaxKind.ColonToken Then
                    expression = DirectCast(RemoveTrailingColonTriviaAndConvertToColonToken(expression, colonToken, excessText), ExpressionSyntax)
                End If
            End If

            Dim alignmentClauseOpt As InterpolationAlignmentClauseSyntax

            If CurrentToken.Kind = SyntaxKind.CommaToken Then
                Debug.Assert(colonToken Is Nothing)

                Dim commaToken = DirectCast(CurrentToken, PunctuationSyntax)
                GetNextToken(ScannerState.VB)

                If CurrentToken.Kind = SyntaxKind.ColonToken Then
                    commaToken = DirectCast(RemoveTrailingColonTriviaAndConvertToColonToken(commaToken, colonToken, excessText), PunctuationSyntax)
                End If

                Dim signTokenOpt As PunctuationSyntax

                If CurrentToken.Kind = SyntaxKind.MinusToken OrElse
                   CurrentToken.Kind = SyntaxKind.PlusToken Then

                    signTokenOpt = DirectCast(CurrentToken, PunctuationSyntax)
                    GetNextToken(ScannerState.VB)

                    If CurrentToken.Kind = SyntaxKind.ColonToken Then
                        signTokenOpt = DirectCast(RemoveTrailingColonTriviaAndConvertToColonToken(signTokenOpt, colonToken, excessText), PunctuationSyntax)
                    End If
                Else
                    signTokenOpt = Nothing
                End If

                Dim widthToken As IntegerLiteralTokenSyntax

                If CurrentToken.Kind = SyntaxKind.IntegerLiteralToken Then

                    widthToken = DirectCast(CurrentToken, IntegerLiteralTokenSyntax)
                    GetNextToken(ScannerState.VB)

                    If CurrentToken.Kind = SyntaxKind.ColonToken Then
                        widthToken = DirectCast(RemoveTrailingColonTriviaAndConvertToColonToken(widthToken, colonToken, excessText), IntegerLiteralTokenSyntax)
                    End If
                Else
                    widthToken = ReportSyntaxError(InternalSyntaxFactory.MissingIntegerLiteralToken(), ERRID.ERR_ExpectedIntLiteral)
                End If

                Dim valueExpression As ExpressionSyntax = SyntaxFactory.NumericLiteralExpression(widthToken)

                If signTokenOpt IsNot Nothing Then
                    valueExpression = SyntaxFactory.UnaryExpression(
                                                        If(signTokenOpt.Kind = SyntaxKind.PlusToken, SyntaxKind.UnaryPlusExpression, SyntaxKind.UnaryMinusExpression),
                                                        signTokenOpt,
                                                        valueExpression)
                End If

                alignmentClauseOpt = SyntaxFactory.InterpolationAlignmentClause(commaToken, valueExpression)
            Else
                alignmentClauseOpt = Nothing
            End If

            Dim formatStringClauseOpt As InterpolationFormatClauseSyntax

            If CurrentToken.Kind = SyntaxKind.ColonToken AndAlso colonToken IsNot Nothing Then
                ' If colonToken IsNot Nothing we were able to recovery.

                GetNextToken(ScannerState.InterpolatedStringFormatString)

                Dim formatStringToken As InterpolatedStringTextTokenSyntax

                If CurrentToken.Kind = SyntaxKind.InterpolatedStringTextToken Then
                    formatStringToken = DirectCast(CurrentToken, InterpolatedStringTextTokenSyntax)
                    GetNextToken(ScannerState.InterpolatedStringPunctuation)

                    If excessText IsNot Nothing Then
                        formatStringToken = InternalSyntaxFactory.InterpolatedStringTextToken(excessText & formatStringToken.Text,
                                                                                              excessText & formatStringToken.Value,
                                                                                              formatStringToken.GetLeadingTrivia(),
                                                                                              formatStringToken.GetTrailingTrivia())
                    End If
                Else
                    If excessText IsNot Nothing Then
                        formatStringToken = InternalSyntaxFactory.InterpolatedStringTextToken(excessText,
                                                                                              excessText,
                                                                                              Nothing,
                                                                                              Nothing)
                    Else
                        formatStringToken = Nothing
                    End If
                End If

                If formatStringToken Is Nothing Then
                    formatStringToken = DirectCast(InternalSyntaxFactory.MissingToken(SyntaxKind.InterpolatedStringTextToken), InterpolatedStringTextTokenSyntax)
                    formatStringToken = ReportSyntaxError(formatStringToken, ERRID.ERR_Syntax)

                ElseIf formatStringToken.GetTrailingTrivia() IsNot Nothing Then
                    formatStringToken = ReportSyntaxError(formatStringToken, ERRID.ERR_InterpolationFormatWhitespace)

                End If

                formatStringClauseOpt = SyntaxFactory.InterpolationFormatClause(colonToken, formatStringToken)

            Else
                formatStringClauseOpt = Nothing

                If CurrentToken.Kind = SyntaxKind.ColonToken Then
                    ' But if colonToken is null we weren't able to gracefully recover.
                    GetNextToken(ScannerState.InterpolatedStringFormatString)
                End If
            End If

            Dim closeBraceToken As PunctuationSyntax

            If CurrentToken.Kind = SyntaxKind.CloseBraceToken Then
                ' Must rescan this with interpolated string rules for trailing trivia.
                ' Specifically, any trailing trivia attached to the closing brace is actually interpolated string content.
                ResetCurrentToken(ScannerState.InterpolatedStringPunctuation)

                closeBraceToken = DirectCast(CurrentToken, PunctuationSyntax)
                GetNextToken(ScannerState.InterpolatedStringContent)

            ElseIf CurrentToken.Kind = SyntaxKind.EndOfInterpolatedStringToken Then
                GetNextToken(ScannerState.VB)

                closeBraceToken = DirectCast(HandleUnexpectedToken(SyntaxKind.CloseBraceToken), PunctuationSyntax)
            Else
                ' Content rules will either resync at a } or at the closing ".
                If Not IsValidStatementTerminator(CurrentToken) Then
                    ResetCurrentToken(ScannerState.InterpolatedStringFormatString)
                End If

                Debug.Assert(CurrentToken.Kind <> SyntaxKind.CloseBraceToken)
                closeBraceToken = DirectCast(HandleUnexpectedToken(SyntaxKind.CloseBraceToken), PunctuationSyntax)

                If CurrentToken.Kind = SyntaxKind.InterpolatedStringTextToken Then
                    ResetCurrentToken(ScannerState.InterpolatedStringContent)
                    GetNextToken(ScannerState.InterpolatedStringContent)
                End If
            End If

            Return SyntaxFactory.Interpolation(openBraceToken, expression, alignmentClauseOpt, formatStringClauseOpt, closeBraceToken)

        End Function

        Private Shared Function RemoveTrailingColonTriviaAndConvertToColonToken(
                             token As SyntaxToken,
                             <Out> ByRef colonToken As PunctuationSyntax,
                             <Out> ByRef excessText As String
                         ) As SyntaxToken

            If Not token.HasTrailingTrivia Then
                colonToken = Nothing
                excessText = Nothing
                Return token
            End If

            Dim triviaList As New CodeAnalysis.Syntax.InternalSyntax.SyntaxList(Of VisualBasicSyntaxNode)(token.GetTrailingTrivia())

            Dim indexOfFirstColon As Integer = -1
            Dim newTrailingTrivia As GreenNode

            If triviaList.Count = 1 Then
                indexOfFirstColon = 0
                newTrailingTrivia = Nothing
                excessText = Nothing
            ElseIf triviaList(0).Kind = SyntaxKind.ColonTrivia
                indexOfFirstColon = 0
                newTrailingTrivia = Nothing
                excessText = triviaList.GetEndOfTrivia(1).Node.ToFullString()
            Else
                For i = 0 To triviaList.Count - 1
                    If triviaList(i).Kind = SyntaxKind.ColonTrivia Then
                        indexOfFirstColon = i
                        Exit For
                    End If
                Next

                newTrailingTrivia = triviaList.GetStartOfTrivia(indexOfFirstColon).Node

                If indexOfFirstColon = triviaList.Count - 1 Then
                    excessText = Nothing
                Else
                    excessText = triviaList.GetEndOfTrivia(indexOfFirstColon + 1).Node.ToFullString()
                    Debug.Assert(triviaList.GetEndOfTrivia(indexOfFirstColon + 1).AnyAndOnly(SyntaxKind.ColonTrivia, SyntaxKind.WhitespaceTrivia))
                End If
            End If

            Dim firstColonTrivia = DirectCast(triviaList(indexOfFirstColon), SyntaxTrivia)

            colonToken = New PunctuationSyntax(SyntaxKind.ColonToken, firstColonTrivia.Text, Nothing, Nothing)

            Return DirectCast(token.WithTrailingTrivia(newTrailingTrivia), SyntaxToken)

        End Function

        Private Function RemoveTrailingColonTriviaAndConvertToColonToken(
                             node As VisualBasicSyntaxNode,
                             <Out> ByRef colonToken As PunctuationSyntax,
                             <Out> ByRef excessText As String
                         ) As VisualBasicSyntaxNode

            Dim lastNonMissing = DirectCast(node.GetLastToken(), SyntaxToken)

            Dim newLastNonMissing = RemoveTrailingColonTriviaAndConvertToColonToken(lastNonMissing, colonToken, excessText)

            Dim newNode = LastTokenReplacer.Replace(node, Function(t) If(t Is lastNonMissing, newLastNonMissing, t))

            ' If no token was replaced we have failed to recover; let high contexts deal with it.
            If newNode Is node Then
                colonToken = Nothing
                excessText = Nothing
            End If

            Return newNode
        End Function

    End Class

End Namespace