/// <summary> /// Scan past the hole inside an interpolated string literal, leaving the current character on the '}' (if any) /// </summary> private void ScanInterpolatedStringLiteralHoleBalancedText(char endingChar, bool isHole, ref int colonPosition) { while (true) { char ch = _lexer.TextWindow.PeekChar(); // Note: within a hole newlines are always allowed. The restriction on if newlines are allowed or not // is only within a text-portion of the interpolated string. if (IsAtEnd(allowNewline: true)) { // the caller will complain return; } switch (ch) { case '#': // preprocessor directives not allowed. TrySetUnrecoverableError(_lexer.MakeError(_lexer.TextWindow.Position, 1, ErrorCode.ERR_SyntaxError, endingChar.ToString())); _lexer.TextWindow.AdvanceChar(); continue; case '$': if (_lexer.TextWindow.PeekChar(1) == '"' || _lexer.TextWindow.PeekChar(1) == '@' && _lexer.TextWindow.PeekChar(2) == '"') { var discarded = default(TokenInfo); _lexer.ScanInterpolatedStringLiteral( isVerbatim: _lexer.TextWindow.PeekChar(1) == '@', ref discarded); continue; } goto default; case ':': // the first colon not nested within matching delimiters is the start of the format string if (isHole) { Debug.Assert(colonPosition == 0); colonPosition = _lexer.TextWindow.Position; ScanFormatSpecifier(); return; } goto default; case '}': case ')': case ']': if (ch == endingChar) { return; } TrySetUnrecoverableError(_lexer.MakeError(_lexer.TextWindow.Position, 1, ErrorCode.ERR_SyntaxError, endingChar.ToString())); goto default; case '"' when RecoveringFromRunawayLexing(): // When recovering from mismatched delimiters, we consume the next // quote character as the close quote for the interpolated string. In // practice this gets us out of trouble in scenarios we've encountered. // See, for example, https://github.com/dotnet/roslyn/issues/44789 return; case '"': case '\'': // handle string or character literal inside an expression hole. ScanInterpolatedStringLiteralNestedString(); continue; case '@': if (_lexer.TextWindow.PeekChar(1) == '"' && !RecoveringFromRunawayLexing()) { // check for verbatim string inside an expression hole. var nestedStringPosition = _lexer.TextWindow.Position; // Note that this verbatim string may encounter an error (for example if it contains a // new line and we don't allow that). This should be reported to the user, but should // not put us into an unrecoverable position. We can clearly see that this was intended // to be a normal verbatim string, so we can continue on an attempt to understand the // outer interpolated string properly. var discarded = default(TokenInfo); var errorCode = _lexer.ScanVerbatimStringLiteral(ref discarded); if (errorCode is ErrorCode code) { TrySetRecoverableError(_lexer.MakeError(nestedStringPosition, width: 2, code)); } continue; } else if (_lexer.TextWindow.PeekChar(1) == '$' && _lexer.TextWindow.PeekChar(2) == '"') { var discarded = default(TokenInfo); _lexer.ScanInterpolatedStringLiteral(isVerbatim: true, ref discarded); continue; } goto default; case '/': switch (_lexer.TextWindow.PeekChar(1)) { case '/': _lexer.ScanToEndOfLine(); continue; case '*': _lexer.ScanMultiLineComment(out _); continue; default: _lexer.TextWindow.AdvanceChar(); continue; } case '{': // TODO: after the colon this has no special meaning. ScanInterpolatedStringLiteralHoleBracketed('{', '}'); continue; case '(': // TODO: after the colon this has no special meaning. ScanInterpolatedStringLiteralHoleBracketed('(', ')'); continue; case '[': // TODO: after the colon this has no special meaning. ScanInterpolatedStringLiteralHoleBracketed('[', ']'); continue; default: // part of code in the expression hole _lexer.TextWindow.AdvanceChar(); continue; } } }
/// <summary> /// Scan past the hole inside an interpolated string literal, leaving the current character on the '}' (if any) /// </summary> private void ScanInterpolatedStringLiteralHoleBalancedText(char endingChar, bool isHole, out Range colonRange) { colonRange = default; while (true) { char ch = _lexer.TextWindow.PeekChar(); // Note: within a hole newlines are always allowed. The restriction on if newlines are allowed or not // is only within a text-portion of the interpolated string. if (IsAtEnd(allowNewline: true)) { // the caller will complain return; } switch (ch) { case '#': // preprocessor directives not allowed. TrySetUnrecoverableError(_lexer.MakeError(_lexer.TextWindow.Position, 1, ErrorCode.ERR_SyntaxError, endingChar.ToString())); _lexer.TextWindow.AdvanceChar(); continue; case '$': { var discarded = default(TokenInfo); if (_lexer.TryScanInterpolatedString(ref discarded)) { continue; } goto default; } case ':': // the first colon not nested within matching delimiters is the start of the format string if (isHole) { Debug.Assert(colonRange.Equals(default(Range))); colonRange = new Range(_lexer.TextWindow.Position, _lexer.TextWindow.Position + 1); ScanFormatSpecifier(); return; } goto default; case '}': case ')': case ']': if (ch == endingChar) { return; } TrySetUnrecoverableError(_lexer.MakeError(_lexer.TextWindow.Position, 1, ErrorCode.ERR_SyntaxError, endingChar.ToString())); goto default; case '"': if (RecoveringFromRunawayLexing()) { // When recovering from mismatched delimiters, we consume the next // quote character as the close quote for the interpolated string. In // practice this gets us out of trouble in scenarios we've encountered. // See, for example, https://github.com/dotnet/roslyn/issues/44789 return; } // handle string literal inside an expression hole. ScanInterpolatedStringLiteralNestedString(); continue; case '\'': // handle character literal inside an expression hole. ScanInterpolatedStringLiteralNestedString(); continue; case '@': { var discarded = default(TokenInfo); if (_lexer.TryScanAtStringToken(ref discarded)) { continue; } // Wasn't an @"" or @$"" string. Just consume this as normal code. goto default; } case '/': switch (_lexer.TextWindow.PeekChar(1)) { case '/': _lexer.ScanToEndOfLine(); continue; case '*': _lexer.ScanMultiLineComment(out _); continue; default: _lexer.TextWindow.AdvanceChar(); continue; } case '{': // TODO: after the colon this has no special meaning. ScanInterpolatedStringLiteralHoleBracketed('{', '}'); continue; case '(': // TODO: after the colon this has no special meaning. ScanInterpolatedStringLiteralHoleBracketed('(', ')'); continue; case '[': // TODO: after the colon this has no special meaning. ScanInterpolatedStringLiteralHoleBracketed('[', ']'); continue; default: // part of code in the expression hole _lexer.TextWindow.AdvanceChar(); continue; } } }