private ExpressionSyntax ParseInterpolatedStringToken() { // We don't want to make the scanner stateful (between tokens) if we can possibly avoid it. // The approach implemented here is // // (1) Scan the whole interpolated string literal as a single token. Now the statefulness of // the scanner (to match { }'s) is limited to its behavior while scanning a single token. // // (2) When the parser gets such a token, here, it spins up another scanner / parser on each of // the holes and builds a tree for the whole thing (resulting in an InterpolatedStringExpressionSyntax). // // (3) The parser discards the original token and replaces it with this tree. (In other words, // it replaces one token with a different set of tokens that have already been parsed) // // (4) On an incremental change, we widen the invalidated region to include any enclosing interpolated // string nonterminal so that we never reuse tokens inside a changed interpolated string. // // This has the secondary advantage that it can reasonably be specified. // // The substitution will end up being invisible to external APIs and clients such as the IDE, as // they have no way to ask for the stream of tokens before parsing. Debug.Assert(this.CurrentToken.Kind == SyntaxKind.InterpolatedStringToken); var originalToken = this.EatToken(); var originalText = originalToken.ValueText; // this is actually the source text Debug.Assert(originalText[0] == '$' || originalText[0] == '@'); // compute the positions of the interpolations in the original string literal, if there was an error or not, // and where the close quote can be found. var interpolations = ArrayBuilder <Lexer.Interpolation> .GetInstance(); rescanInterpolation(out var kind, out var error, out var openQuoteRange, interpolations, out var closeQuoteRange); var result = SyntaxFactory.InterpolatedStringExpression( getOpenQuote(openQuoteRange), getContent(interpolations), getCloseQuote(closeQuoteRange)); interpolations.Free(); if (error != null) { result = result.WithDiagnosticsGreen(new[] { error }); } Debug.Assert(originalToken.ToFullString() == result.ToFullString()); // yield from text equals yield from node return(result); void rescanInterpolation(out Lexer.InterpolatedStringKind kind, out SyntaxDiagnosticInfo error, out Range openQuoteRange, ArrayBuilder <Lexer.Interpolation> interpolations, out Range closeQuoteRange) { using var tempLexer = new Lexer(SourceText.From(originalText), this.Options, allowPreprocessorDirectives: false); var info = default(Lexer.TokenInfo); tempLexer.ScanInterpolatedStringLiteralTop(ref info, out error, out kind, out openQuoteRange, interpolations, out closeQuoteRange); } SyntaxToken getOpenQuote(Range openQuoteRange) { var openQuoteText = originalText[openQuoteRange]; return(SyntaxFactory.Token( originalToken.GetLeadingTrivia(), kind is Lexer.InterpolatedStringKind.Verbatim ? SyntaxKind.InterpolatedVerbatimStringStartToken : SyntaxKind.InterpolatedStringStartToken, openQuoteText, openQuoteText, trailing: null)); } CodeAnalysis.Syntax.InternalSyntax.SyntaxList <InterpolatedStringContentSyntax> getContent(ArrayBuilder <Lexer.Interpolation> interpolations) { var builder = _pool.Allocate <InterpolatedStringContentSyntax>(); if (interpolations.Count == 0) { // In the special case when there are no interpolations, we just construct a format string // with no inserts. We must still use String.Format to get its handling of escapes such as {{, // so we still treat it as a composite format string. var text = originalText[new Range(openQuoteRange.End, closeQuoteRange.Start)]; if (text.Length > 0) { builder.Add(SyntaxFactory.InterpolatedStringText(MakeInterpolatedStringTextToken(text, kind))); } } else { for (int i = 0; i < interpolations.Count; i++) { var interpolation = interpolations[i]; // Add a token for text preceding the interpolation var text = originalText[new Range( i == 0 ? openQuoteRange.End : interpolations[i - 1].CloseBraceRange.End, interpolation.OpenBraceRange.Start)]; if (text.Length > 0) { builder.Add(SyntaxFactory.InterpolatedStringText(MakeInterpolatedStringTextToken(text, kind))); } builder.Add(ParseInterpolation(this.Options, originalText, interpolation, kind)); } // Add a token for text following the last interpolation var lastText = originalText[new Range(interpolations[^ 1].CloseBraceRange.End, closeQuoteRange.Start)];
private ExpressionSyntax ParseInterpolatedStringToken() { // We don't want to make the scanner stateful (between tokens) if we can possibly avoid it. // The approach implemented here is // // (1) Scan the whole interpolated string literal as a single token. Now the statefulness of // the scanner (to match { }'s) is limited to its behavior while scanning a single token. // // (2) When the parser gets such a token, here, it spins up another scanner / parser on each of // the holes and builds a tree for the whole thing (resulting in an InterpolatedStringExpressionSyntax). // // (3) The parser discards the original token and replaces it with this tree. (In other words, // it replaces one token with a different set of tokens that have already been parsed) // // (4) On an incremental change, we widen the invalidated region to include any enclosing interpolated // string nonterminal so that we never reuse tokens inside a changed interpolated string. // // This has the secondary advantage that it can reasonably be specified. // // The substitution will end up being invisible to external APIs and clients such as the IDE, as // they have no way to ask for the stream of tokens before parsing. // var originalToken = this.EatToken(); var originalText = originalToken.ValueText; // this is actually the source text Debug.Assert(originalText[0] == '$' || originalText[0] == '@'); var isAltInterpolatedVerbatim = originalText.Length > 2 && originalText[0] == '@'; // @$ var isVerbatim = isAltInterpolatedVerbatim || (originalText.Length > 2 && originalText[1] == '@'); Debug.Assert(originalToken.Kind == SyntaxKind.InterpolatedStringToken); var interpolations = ArrayBuilder <Lexer.Interpolation> .GetInstance(); SyntaxDiagnosticInfo error = null; bool closeQuoteMissing; using (var tempLexer = new Lexer(Text.SourceText.From(originalText), this.Options, allowPreprocessorDirectives: false)) { // compute the positions of the interpolations in the original string literal, and also compute/preserve // lexical errors var info = default(Lexer.TokenInfo); tempLexer.ScanInterpolatedStringLiteralTop(interpolations, isVerbatim, ref info, ref error, out closeQuoteMissing); } // Make a token for the open quote $" or $@" or @$" var openQuoteIndex = isVerbatim ? 2 : 1; Debug.Assert(originalText[openQuoteIndex] == '"'); var openQuoteKind = isVerbatim ? SyntaxKind.InterpolatedVerbatimStringStartToken // $@ or @$ : SyntaxKind.InterpolatedStringStartToken; // $ var openQuoteText = isAltInterpolatedVerbatim ? "@$\"" : isVerbatim ? "$@\"" : "$\""; var openQuote = SyntaxFactory.Token(originalToken.GetLeadingTrivia(), openQuoteKind, openQuoteText, openQuoteText, trailing: null); if (isAltInterpolatedVerbatim) { openQuote = CheckFeatureAvailability(openQuote, MessageID.IDS_FeatureAltInterpolatedVerbatimStrings); } // Make a token for the close quote " (even if it was missing) var closeQuoteIndex = closeQuoteMissing ? originalText.Length : originalText.Length - 1; Debug.Assert(closeQuoteMissing || originalText[closeQuoteIndex] == '"'); var closeQuote = closeQuoteMissing ? SyntaxFactory.MissingToken(SyntaxKind.InterpolatedStringEndToken).TokenWithTrailingTrivia(originalToken.GetTrailingTrivia()) : SyntaxFactory.Token(null, SyntaxKind.InterpolatedStringEndToken, originalToken.GetTrailingTrivia()); var builder = _pool.Allocate <InterpolatedStringContentSyntax>(); if (interpolations.Count == 0) { // In the special case when there are no interpolations, we just construct a format string // with no inserts. We must still use String.Format to get its handling of escapes such as {{, // so we still treat it as a composite format string. var text = Substring(originalText, openQuoteIndex + 1, closeQuoteIndex - 1); if (text.Length > 0) { var token = MakeStringToken(text, text, isVerbatim, SyntaxKind.InterpolatedStringTextToken); builder.Add(SyntaxFactory.InterpolatedStringText(token)); } } else { for (int i = 0; i < interpolations.Count; i++) { var interpolation = interpolations[i]; // Add a token for text preceding the interpolation var text = Substring(originalText, (i == 0) ? (openQuoteIndex + 1) : (interpolations[i - 1].CloseBracePosition + 1), interpolation.OpenBracePosition - 1); if (text.Length > 0) { var token = MakeStringToken(text, text, isVerbatim, SyntaxKind.InterpolatedStringTextToken); builder.Add(SyntaxFactory.InterpolatedStringText(token)); } // Add an interpolation var interp = ParseInterpolation(originalText, interpolation, isVerbatim); builder.Add(interp); } // Add a token for text following the last interpolation var lastText = Substring(originalText, interpolations[interpolations.Count - 1].CloseBracePosition + 1, closeQuoteIndex - 1); if (lastText.Length > 0) { var token = MakeStringToken(lastText, lastText, isVerbatim, SyntaxKind.InterpolatedStringTextToken); builder.Add(SyntaxFactory.InterpolatedStringText(token)); } } interpolations.Free(); var result = SyntaxFactory.InterpolatedStringExpression(openQuote, builder, closeQuote); _pool.Free(builder); if (error != null) { result = result.WithDiagnosticsGreen(new[] { error }); } Debug.Assert(originalToken.ToFullString() == result.ToFullString()); // yield from text equals yield from node return(CheckFeatureAvailability(result, MessageID.IDS_FeatureInterpolatedStrings)); }
private ExpressionSyntax ParseInterpolatedStringToken() { // We don't want to make the scanner stateful (between tokens) if we can possibly avoid it. // The approach implemented here is // // (1) Scan the whole interpolated string literal as a single token. Now the statefulness of // the scanner (to match { }'s) is limited to its behavior while scanning a single token. // // (2) When the parser gets such a token, here, it spins up another scanner / parser on each of // the holes and builds a tree for the whole thing (resulting in an InterpolatedStringExpressionSyntax). // // (3) The parser discards the original token and replaces it with this tree. (In other words, // it replaces one token with a different set of tokens that have already been parsed) // // (4) On an incremental change, we widen the invalidated region to include any enclosing interpolated // string nonterminal so that we never reuse tokens inside a changed interpolated string. // // This has the secondary advantage that it can reasonably be specified. // // The substitution will end up being invisible to external APIs and clients such as the IDE, as // they have no way to ask for the stream of tokens before parsing. Debug.Assert(this.CurrentToken.Kind == SyntaxKind.InterpolatedStringToken); var originalToken = this.EatToken(); var originalText = originalToken.ValueText; // this is actually the source text var originalTextSpan = originalText.AsSpan(); Debug.Assert(originalText[0] == '$' || originalText[0] == '@'); // compute the positions of the interpolations in the original string literal, if there was an error or not, // and where the open and close quotes can be found. var interpolations = ArrayBuilder <Lexer.Interpolation> .GetInstance(); rescanInterpolation(out var kind, out var error, out var openQuoteRange, interpolations, out var closeQuoteRange); // Only bother trying to do dedentation if we have a multiline literal without errors. There's no point // trying in the presence of errors as we may not even be able to determine what the dedentation should be. var needsDedentation = kind == Lexer.InterpolatedStringKind.MultiLineRaw && error == null; var result = SyntaxFactory.InterpolatedStringExpression(getOpenQuote(), getContent(originalTextSpan), getCloseQuote()); interpolations.Free(); if (error != null) { // Errors are positioned relative to the start of the token that was lexed. Specifically relative to // the starting `$` or `@`. However, when placed on a node like this, it will be relative to the node's // full start. So we have to adjust the diagnostics taking that into account. result = result.WithDiagnosticsGreen(MoveDiagnostics(new[] { error }, originalToken.GetLeadingTrivia()?.FullWidth ?? 0)); } Debug.Assert(originalToken.ToFullString() == result.ToFullString()); // yield from text equals yield from node return(result); void rescanInterpolation(out Lexer.InterpolatedStringKind kind, out SyntaxDiagnosticInfo?error, out Range openQuoteRange, ArrayBuilder <Lexer.Interpolation> interpolations, out Range closeQuoteRange) { using var tempLexer = new Lexer(SourceText.From(originalText), this.Options, allowPreprocessorDirectives: false); var info = default(Lexer.TokenInfo); tempLexer.ScanInterpolatedStringLiteralTop(ref info, out error, out kind, out openQuoteRange, interpolations, out closeQuoteRange); } SyntaxToken getOpenQuote() { return(SyntaxFactory.Token( originalToken.GetLeadingTrivia(), kind switch { Lexer.InterpolatedStringKind.Normal => SyntaxKind.InterpolatedStringStartToken, Lexer.InterpolatedStringKind.Verbatim => SyntaxKind.InterpolatedVerbatimStringStartToken, Lexer.InterpolatedStringKind.SingleLineRaw => SyntaxKind.InterpolatedSingleLineRawStringStartToken, Lexer.InterpolatedStringKind.MultiLineRaw => SyntaxKind.InterpolatedMultiLineRawStringStartToken, _ => throw ExceptionUtilities.UnexpectedValue(kind), }, originalText[openQuoteRange], trailing: null)); }