From 09a7b388974b48af9ef28a539de10cb463014e15 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 24 Oct 2024 23:56:57 -0700 Subject: [PATCH] Do not lex `..` as a single DotDotToken (#75549) * Add failing test * WorkItem :) * Renames and docs * Update tests * Do not have lexer produce .. token * Move error * Docs * Advance * Place at the same locaiton * Cleanup and consistency * Break apart numerics * in progress * Lexer work * restore * Update src/Compilers/CSharp/Test/Syntax/Parsing/ParsingTests.cs * Simplify * Update quick scanner as well * More cases * Consequence parsing * Fixup test * More precise errors * Specialized error message * Simplify expression parsing * Extract helper * simplify * simplify * simplify * inline * Simplify token merging * Fix * Simplify * Simplify * Simplify * Update src/Compilers/CSharp/Portable/Parser/LanguageParser.cs Co-authored-by: Rikki Gibson * Tweak comment * Grammar * Update src/Compilers/CSharp/Portable/Parser/LanguageParser.cs * Lower value slightly * Fix * Clean up usings * Add tests * Simplify * Add tests * Add tests --------- Co-authored-by: Rekkonnect <8298332+Rekkonnect@users.noreply.github.com> Co-authored-by: Rikki Gibson --- .../CSharp/Portable/Parser/Blender.Reader.cs | 4 +- .../CSharp/Portable/Parser/Blender.cs | 3 - .../CSharp/Portable/Parser/LanguageParser.cs | 161 +++++---- .../Parser/LanguageParser_Patterns.cs | 9 +- src/Compilers/CSharp/Portable/Parser/Lexer.cs | 43 ++- .../CSharp/Portable/Parser/QuickScanner.cs | 5 +- .../CSharp/Portable/Parser/SyntaxParser.cs | 2 +- .../IncrementalParsingTests.cs | 264 ++++++++++++++- .../CollectionExpressionParsingTests.cs | 17 +- .../Syntax/Parsing/ExpressionParsingTests.cs | 4 +- .../Parsing/MemberDeclarationParsingTests.cs | 15 +- .../PatternParsingTests_ListPatterns.cs | 13 +- .../Parsing/RangeExpressionParsingTests.cs | 318 ++++++++++++++++++ 13 files changed, 726 insertions(+), 132 deletions(-) create mode 100644 src/Compilers/CSharp/Test/Syntax/Parsing/RangeExpressionParsingTests.cs diff --git a/src/Compilers/CSharp/Portable/Parser/Blender.Reader.cs b/src/Compilers/CSharp/Portable/Parser/Blender.Reader.cs index 4181b444e1c75..eb0a967120e9f 100644 --- a/src/Compilers/CSharp/Portable/Parser/Blender.Reader.cs +++ b/src/Compilers/CSharp/Portable/Parser/Blender.Reader.cs @@ -6,10 +6,7 @@ using System.Collections.Immutable; using System.Diagnostics; -using Microsoft.CodeAnalysis.CSharp.Symbols; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax { @@ -307,6 +304,7 @@ internal static bool IsFabricatedToken(SyntaxKind kind) case SyntaxKind.GreaterThanGreaterThanEqualsToken: case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case SyntaxKind.DotDotToken: return true; default: return SyntaxFacts.IsContextualKeyword(kind); diff --git a/src/Compilers/CSharp/Portable/Parser/Blender.cs b/src/Compilers/CSharp/Portable/Parser/Blender.cs index 0f5e154d0d7ec..064c234f940cd 100644 --- a/src/Compilers/CSharp/Portable/Parser/Blender.cs +++ b/src/Compilers/CSharp/Portable/Parser/Blender.cs @@ -8,10 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using Microsoft.CodeAnalysis.CSharp.Symbols; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax { diff --git a/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs b/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs index 80aa9f19c9a19..d96514ff2769c 100644 --- a/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs +++ b/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs @@ -3235,7 +3235,6 @@ private bool IsFieldDeclaration(bool isEvent, bool isGlobalScriptLevel) { case SyntaxKind.DotToken: // Goo. explicit case SyntaxKind.ColonColonToken: // Goo:: explicit - case SyntaxKind.DotDotToken: // Goo.. explicit case SyntaxKind.LessThanToken: // Goo< explicit or generic method case SyntaxKind.OpenBraceToken: // Goo { property case SyntaxKind.EqualsGreaterThanToken: // Goo => property @@ -3563,12 +3562,12 @@ private ConversionOperatorDeclarationSyntax TryParseConversionOperatorDeclaratio IsMakingProgress(ref lastTokenPosition, assertIfFalse: true); ScanNamedTypePart(); - if (IsDotOrColonColonOrDotDot() || + if (IsDotOrColonColon() || (IsMakingProgress(ref lastTokenPosition, assertIfFalse: false) && this.CurrentToken.Kind != SyntaxKind.OpenParenToken)) { haveExplicitInterfaceName = true; - if (IsDotOrColonColonOrDotDot()) + if (IsDotOrColonColon()) { separatorKind = this.CurrentToken.Kind; EatToken(); @@ -3593,7 +3592,7 @@ private ConversionOperatorDeclarationSyntax TryParseConversionOperatorDeclaratio bool possibleConversion; if (this.CurrentToken.Kind != SyntaxKind.OperatorKeyword || - (haveExplicitInterfaceName && separatorKind is not (SyntaxKind.DotToken or SyntaxKind.DotDotToken))) + (haveExplicitInterfaceName && separatorKind is not SyntaxKind.DotToken)) { possibleConversion = false; } @@ -3718,8 +3717,8 @@ ExplicitInterfaceSpecifierSyntax tryParseExplicitInterfaceSpecifier() int lastTokenPosition = -1; IsMakingProgress(ref lastTokenPosition, assertIfFalse: true); ScanNamedTypePart(); - isPartOfInterfaceName = IsDotOrColonColonOrDotDot() || - (IsMakingProgress(ref lastTokenPosition, assertIfFalse: false) && this.CurrentToken.Kind != SyntaxKind.OpenParenToken); + isPartOfInterfaceName = IsDotOrColonColon() || + (IsMakingProgress(ref lastTokenPosition, assertIfFalse: false) && this.CurrentToken.Kind != SyntaxKind.OpenParenToken); } } @@ -3841,9 +3840,10 @@ private MemberDeclarationSyntax ParseOperatorDeclaration( } else { - //Consume whatever follows the operator keyword as the operator token. If it is not - //we'll add an error below (when we can guess the arity). - opToken = EatToken(); + // Consume whatever follows the operator keyword as the operator token. If it is not we'll add an + // error below (when we can guess the arity). Handle .. as well so we can give the user a good + // message if they do `operator ..` + opToken = IsAtDotDotToken() ? EatDotDotToken() : EatToken(); Debug.Assert(!opToken.IsMissing); opTokenErrorOffset = opToken.GetLeadingTriviaWidth(); opTokenErrorWidth = opToken.Width; @@ -6425,7 +6425,7 @@ private void ParseMemberName( using (GetDisposableResetPoint(resetOnDispose: true)) { ScanNamedTypePart(); - isMemberName = !IsDotOrColonColonOrDotDot(); + isMemberName = !IsDotOrColonColon(); } if (isMemberName) @@ -6531,18 +6531,9 @@ private void AccumulateExplicitInterfaceName(ref NameSyntax explicitInterfaceNam explicitInterfaceName = this.ParseSimpleName(NameOptions.InTypeList); // Now, get the next separator. - if (this.CurrentToken.Kind == SyntaxKind.DotDotToken) - { - // Error recovery as in ParseQualifiedNameRight. If we have `X..Y` break that into `X..Y` - separator = this.EatToken(); - explicitInterfaceName = RecoverFromDotDot(explicitInterfaceName, ref separator); - } - else - { - separator = this.CurrentToken.Kind == SyntaxKind.ColonColonToken - ? this.EatToken() // fine after the first identifier - : this.EatToken(SyntaxKind.DotToken); - } + separator = this.CurrentToken.Kind == SyntaxKind.ColonColonToken + ? this.EatToken() // fine after the first identifier + : this.EatToken(SyntaxKind.DotToken); } else { @@ -6559,12 +6550,6 @@ private void AccumulateExplicitInterfaceName(ref NameSyntax explicitInterfaceNam separator = this.AddError(separator, ErrorCode.ERR_UnexpectedAliasedName); separator = this.ConvertToMissingWithTrailingTrivia(separator, SyntaxKind.DotToken); } - else if (this.CurrentToken.Kind == SyntaxKind.DotDotToken) - { - // Error recovery as in ParseQualifiedNameRight. If we have `X..Y` break that into `X..Y` - separator = this.EatToken(); - explicitInterfaceName = RecoverFromDotDot(explicitInterfaceName, ref separator); - } else { separator = this.EatToken(SyntaxKind.DotToken); @@ -6617,7 +6602,7 @@ private bool IsOperatorStart(out ExplicitInterfaceSpecifierSyntax explicitInterf // If we have part of the interface name, but no dot before the operator token, then // for the purpose of error recovery, treat this as an operator start with a // missing dot token. - isPartOfInterfaceName = IsDotOrColonColonOrDotDot() || IsOperatorKeyword(); + isPartOfInterfaceName = IsDotOrColonColon() || IsOperatorKeyword(); } } @@ -6674,7 +6659,7 @@ private NameSyntax ParseQualifiedName(NameOptions options = NameOptions.None) NameSyntax name = this.ParseAliasQualifiedName(options); // Handle .. tokens for error recovery purposes. - while (IsDotOrColonColonOrDotDot()) + while (IsDotOrColonColon()) { if (this.PeekToken(1).Kind == SyntaxKind.ThisKeyword) { @@ -6688,26 +6673,18 @@ private NameSyntax ParseQualifiedName(NameOptions options = NameOptions.None) return name; } - private bool IsDotOrColonColonOrDotDot() - { - return this.IsDotOrColonColon() || this.CurrentToken.Kind == SyntaxKind.DotDotToken; - } - private NameSyntax ParseQualifiedNameRight( NameOptions options, NameSyntax left, SyntaxToken separator) { - Debug.Assert(separator.Kind is SyntaxKind.DotToken or SyntaxKind.DotDotToken or SyntaxKind.ColonColonToken); + Debug.Assert(separator.Kind is SyntaxKind.DotToken or SyntaxKind.ColonColonToken); var right = this.ParseSimpleName(options); switch (separator.Kind) { case SyntaxKind.DotToken: return _syntaxFactory.QualifiedName(left, separator, right); - case SyntaxKind.DotDotToken: - // Error recovery. If we have `X..Y` break that into `X..Y` - return _syntaxFactory.QualifiedName(RecoverFromDotDot(left, ref separator), separator, right); case SyntaxKind.ColonColonToken: if (left.Kind != SyntaxKind.IdentifierName) @@ -6741,16 +6718,6 @@ private NameSyntax ParseQualifiedNameRight( } } - private NameSyntax RecoverFromDotDot(NameSyntax left, ref SyntaxToken separator) - { - Debug.Assert(separator.Kind == SyntaxKind.DotDotToken); - - var leftDot = SyntaxFactory.Token(separator.LeadingTrivia.Node, SyntaxKind.DotToken, null); - var missingName = this.AddError(this.CreateMissingIdentifierName(), ErrorCode.ERR_IdentifierExpected); - separator = SyntaxFactory.Token(null, SyntaxKind.DotToken, separator.TrailingTrivia.Node); - return _syntaxFactory.QualifiedName(left, leftDot, missingName); - } - private SyntaxToken ConvertToMissingWithTrailingTrivia(SyntaxToken token, SyntaxKind expectedKind) { var newToken = SyntaxFactory.MissingToken(expectedKind); @@ -7878,6 +7845,8 @@ private SyntaxList ParseStatementAttributeDeclarations() // Check the next token to see if it indicates the `[...]` sequence we have is a term or not. This is the // same set of tokens that ParsePostFixExpression looks for. + // + // Note `SyntaxKind.DotToken` handles both the `[...].Name` case as well as the `[...]..Range` case. var isCollectionExpression = this.CurrentToken.Kind is SyntaxKind.DotToken or SyntaxKind.QuestionToken @@ -7893,7 +7862,6 @@ or SyntaxKind.MinusMinusToken isCollectionExpression = isCollectionExpression || IsExpectedBinaryOperator(this.CurrentToken.Kind) || IsExpectedAssignmentOperator(this.CurrentToken.Kind) - || this.CurrentToken.Kind is SyntaxKind.DotDotToken || (this.CurrentToken.ContextualKind is SyntaxKind.SwitchKeyword or SyntaxKind.WithKeyword && this.PeekToken(1).Kind is SyntaxKind.OpenBraceToken); if (!isCollectionExpression && @@ -10616,10 +10584,11 @@ private bool IsPossibleExpression(bool allowBinaryExpressions, bool allowAssignm case SyntaxKind.ColonColonToken: // bad aliased name case SyntaxKind.ThrowKeyword: case SyntaxKind.StackAllocKeyword: - case SyntaxKind.DotDotToken: case SyntaxKind.RefKeyword: case SyntaxKind.OpenBracketToken: // attributes on a lambda, or a collection expression. return true; + case SyntaxKind.DotToken when IsAtDotDotToken(): + return true; case SyntaxKind.StaticKeyword: return IsPossibleAnonymousMethodExpression() || IsPossibleLambdaExpression(Precedence.Expression); case SyntaxKind.IdentifierToken: @@ -10966,11 +10935,11 @@ ExpressionSyntax parseUnaryOrPrimaryExpression(Precedence precedence) // Check *explicitly* for `..` starting an expression. This *is* the initial term we want to parse out. // If we have `expr..` though we don't do that here. Instead, we'll parse out 'expr', and the `..` // portion will be handled in ParseExpressionContinued. - if (tk == SyntaxKind.DotDotToken) + if (IsAtDotDotToken()) { return _syntaxFactory.RangeExpression( leftOperand: null, - this.EatToken(), + this.EatDotDotToken(), CanStartExpression() ? this.ParseSubExpression(Precedence.Range) : null); @@ -11069,7 +11038,6 @@ private ExpressionSyntax ParseExpressionContinued(ExpressionSyntax unaryOrPrimar return null; // Now consume the operator (including consuming multiple tokens in the case of merged operator tokens) - var operatorToken = eatOperatorToken(operatorTokenKind); if (newPrecedence > GetPrecedence(leftOperand.Kind)) @@ -11152,6 +11120,10 @@ private ExpressionSyntax ParseExpressionContinued(ExpressionSyntax unaryOrPrimar var token1 = this.CurrentToken; var token1Kind = token1.ContextualKind; + // Merge two consecutive dots into a DotDotToken + if (IsAtDotDotToken()) + return (SyntaxKind.DotDotToken, SyntaxKind.RangeExpression); + // check for >>, >>=, >>> or >>>= // // In all those cases, update token1Kind to be the merged token kind. It will then be handled by the code below. @@ -11188,9 +11160,6 @@ private ExpressionSyntax ParseExpressionContinued(ExpressionSyntax unaryOrPrimar if (IsExpectedAssignmentOperator(token1Kind)) return (token1Kind, SyntaxFacts.GetAssignmentExpression(token1Kind)); - if (token1Kind == SyntaxKind.DotDotToken) - return (token1Kind, SyntaxKind.RangeExpression); - if (token1Kind == SyntaxKind.SwitchKeyword && this.PeekToken(1).Kind == SyntaxKind.OpenBraceToken) return (token1Kind, SyntaxKind.SwitchExpression); @@ -11205,6 +11174,10 @@ private ExpressionSyntax ParseExpressionContinued(ExpressionSyntax unaryOrPrimar SyntaxToken eatOperatorToken(SyntaxKind operatorTokenKind) { // Combine tokens into a single token if needed + + if (operatorTokenKind is SyntaxKind.DotDotToken) + return EatDotDotToken(); + if (operatorTokenKind is SyntaxKind.GreaterThanGreaterThanToken or SyntaxKind.GreaterThanGreaterThanEqualsToken) { // >> and >>= @@ -11333,6 +11306,52 @@ static bool containsTernaryCollectionToReinterpret(ExpressionSyntax expression) } } + /// Check if we're currently at a .. sequence that can then be parsed out as a . + public bool IsAtDotDotToken() + => IsAtDotDotToken(this.CurrentToken, this.PeekToken(1)); + + public static bool IsAtDotDotToken(SyntaxToken token1, SyntaxToken token2) + => token1.Kind == SyntaxKind.DotToken && + token2.Kind == SyntaxKind.DotToken && + NoTriviaBetween(token1, token2); + + /// Consume the next two tokens as a . Note: if three dot tokens + /// are in a row, an error will be placed on the .. token to say that is illegal, and single DotDot token + /// will be returned. + public SyntaxToken EatDotDotToken() + { + Debug.Assert(IsAtDotDotToken()); + var token1 = this.EatToken(); + var token2 = this.EatToken(); + + var dotDotToken = SyntaxFactory.Token(token1.GetLeadingTrivia(), SyntaxKind.DotDotToken, token2.GetTrailingTrivia()); + if (this.CurrentToken is { Kind: SyntaxKind.DotToken } token3 && + NoTriviaBetween(token2, token3)) + { + // At least three dots directly in a row. Definitely mark that this is always illegal. We do not allow + // `...` at all in case we want to use that syntax in the future. + dotDotToken = AddError( + dotDotToken, + offset: dotDotToken.GetLeadingTriviaWidth(), + length: 0, + ErrorCode.ERR_TripleDotNotAllowed); + + // If we have exactly 3 dots in a row, then make the third dot into skipped trivia on the `..` as this + // is likely just a mistyped range/slice and we'll recover better if we don't try to process the 3rd dot + // as a member access or anything like that. + // + // If we have 4 dots in a row (`....`), then don't skip any of them. We'll let the caller handle the + // next two dots as a range/slice/whatever. + if (this.PeekToken(1) is not { Kind: SyntaxKind.DotToken } token4 || + !NoTriviaBetween(token3, token4)) + { + dotDotToken = AddSkippedSyntax(dotDotToken, this.EatToken(), trailing: true); + } + } + + return dotDotToken; + } + #nullable disable private DeclarationExpressionSyntax ParseDeclarationExpression(ParseTypeMode mode, bool isScoped) @@ -11580,7 +11599,7 @@ ExpressionSyntax parsePostFixExpression(ExpressionSyntax expr) expr = _syntaxFactory.MemberAccessExpression(SyntaxKind.PointerMemberAccessExpression, expr, this.EatToken(), this.ParseSimpleName(NameOptions.InExpression)); continue; - case SyntaxKind.DotToken: + case SyntaxKind.DotToken when !IsAtDotDotToken(): // if we have the error situation: // // expr. @@ -11708,10 +11727,13 @@ private bool IsPossibleAnonymousMethodExpression() private bool CanStartConsequenceExpression() { Debug.Assert(this.CurrentToken.Kind == SyntaxKind.QuestionToken); - var nextTokenKind = this.PeekToken(1).Kind; + var nextToken = this.PeekToken(1); + var nextTokenKind = nextToken.Kind; - // ?. is always the start of of a consequence expression. - if (nextTokenKind == SyntaxKind.DotToken) + // ?. is always the start of of a consequence expression. + // + // ?.. is a ternary with a range expression as it's 'whenTrue' clause. + if (nextTokenKind == SyntaxKind.DotToken && !IsAtDotDotToken(nextToken, this.PeekToken(2))) return true; if (nextTokenKind == SyntaxKind.OpenBracketToken) @@ -12364,8 +12386,11 @@ private bool ScanCast(bool forPattern = false) SyntaxKind.PlusToken or SyntaxKind.MinusToken or SyntaxKind.AmpersandToken or - SyntaxKind.AsteriskToken or - SyntaxKind.DotDotToken + SyntaxKind.AsteriskToken + => true, + + // `(X)..` must be a cast of a range expression, not a member access of some arbitrary expression. + SyntaxKind.DotToken when IsAtDotDotToken() => true, var tk @@ -12618,7 +12643,6 @@ private static bool CanFollowCast(SyntaxKind kind) case SyntaxKind.EndOfFileToken: case SyntaxKind.SwitchKeyword: case SyntaxKind.EqualsGreaterThanToken: - case SyntaxKind.DotDotToken: return false; default: return true; @@ -12680,12 +12704,9 @@ private bool IsPossibleCollectionElement() private CollectionElementSyntax ParseCollectionElement() { - var dotDotToken = this.TryEatToken(SyntaxKind.DotDotToken); - if (dotDotToken != null) - return _syntaxFactory.SpreadElement(dotDotToken, this.ParseExpressionCore()); - - var expression = this.ParseExpressionCore(); - return _syntaxFactory.ExpressionElement(expression); + return IsAtDotDotToken() + ? _syntaxFactory.SpreadElement(this.EatDotDotToken(), this.ParseExpressionCore()) + : _syntaxFactory.ExpressionElement(this.ParseExpressionCore()); } private bool IsAnonymousType() diff --git a/src/Compilers/CSharp/Portable/Parser/LanguageParser_Patterns.cs b/src/Compilers/CSharp/Portable/Parser/LanguageParser_Patterns.cs index ad961242a9386..712ddc1633420 100644 --- a/src/Compilers/CSharp/Portable/Parser/LanguageParser_Patterns.cs +++ b/src/Compilers/CSharp/Portable/Parser/LanguageParser_Patterns.cs @@ -193,9 +193,12 @@ private PatternSyntax ParsePrimaryPattern(Precedence precedence, bool afterIs, b { case SyntaxKind.OpenBracketToken: return this.ParseListPattern(inSwitchArmPattern); - case SyntaxKind.DotDotToken: - return _syntaxFactory.SlicePattern(EatToken(), - IsPossibleSubpatternElement() ? ParsePattern(precedence, afterIs: false, inSwitchArmPattern) : null); + case SyntaxKind.DotToken when IsAtDotDotToken(): + return _syntaxFactory.SlicePattern( + EatDotDotToken(), + IsPossibleSubpatternElement() + ? ParsePattern(precedence, afterIs: false, inSwitchArmPattern) + : null); case SyntaxKind.LessThanToken: case SyntaxKind.LessThanEqualsToken: case SyntaxKind.GreaterThanToken: diff --git a/src/Compilers/CSharp/Portable/Parser/Lexer.cs b/src/Compilers/CSharp/Portable/Parser/Lexer.cs index bcb3d9296d2ad..a34f9cdd9b3a3 100644 --- a/src/Compilers/CSharp/Portable/Parser/Lexer.cs +++ b/src/Compilers/CSharp/Portable/Parser/Lexer.cs @@ -68,7 +68,7 @@ internal enum XmlDocCommentStyle Delimited = 1 } - internal partial class Lexer : AbstractLexer + internal sealed partial class Lexer : AbstractLexer { private const int TriviaListInitialCapacity = 8; @@ -459,26 +459,41 @@ private void ScanSyntaxToken(ref TokenInfo info) break; case '.': - if (!this.ScanNumericLiteral(ref info)) + if (this.TextWindow.PeekChar(1) is >= '0' and <= '9') { - TextWindow.AdvanceChar(); - if (TextWindow.TryAdvance('.')) + var atDotPosition = this.TextWindow.Position; + if (atDotPosition >= 1 && + atDotPosition == this.TextWindow.LexemeStartPosition) { - if (TextWindow.PeekChar() == '.') + // We have something like: .0 + // + // This could be a fp number *except* the case where we have `..0` in that case, we want two + // dots followed by an integer (which will be treated as a range expression). + // + // Move back one space to see what's before this dot and adjust accordingly. + + this.TextWindow.Reset(atDotPosition - 1); + var priorCharacterIsDot = this.TextWindow.PeekChar() is '.'; + this.TextWindow.Reset(atDotPosition); + + if (priorCharacterIsDot) { - // Triple-dot: explicitly reject this, to allow triple-dot - // to be added to the language without a breaking change. - // (without this, 0...2 would parse as (0)..(.2), i.e. a range from 0 to 0.2) - this.AddError(ErrorCode.ERR_TripleDotNotAllowed); + // We have two dots in a row. Treat the second dot as a dot, not the start of a number literal. + TextWindow.AdvanceChar(); + info.Kind = SyntaxKind.DotToken; + break; } - info.Kind = SyntaxKind.DotDotToken; - } - else - { - info.Kind = SyntaxKind.DotToken; + // Fall through naturally and scan the number out as a floating point number. } } + + if (!this.ScanNumericLiteral(ref info)) + { + TextWindow.AdvanceChar(); + info.Kind = SyntaxKind.DotToken; + } + break; case ',': diff --git a/src/Compilers/CSharp/Portable/Parser/QuickScanner.cs b/src/Compilers/CSharp/Portable/Parser/QuickScanner.cs index 13d159ced8b5d..0084d14b53f80 100644 --- a/src/Compilers/CSharp/Portable/Parser/QuickScanner.cs +++ b/src/Compilers/CSharp/Portable/Parser/QuickScanner.cs @@ -4,9 +4,6 @@ using System; using System.Diagnostics; -using Microsoft.CodeAnalysis.CSharp.Symbols; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax @@ -152,7 +149,7 @@ private enum CharFlags : byte (byte)QuickScanState.FollowingCR, // CR (byte)QuickScanState.DoneAfterNext, // LF (byte)QuickScanState.Done, // Letter - (byte)QuickScanState.Number, // Digit + (byte)QuickScanState.Bad, // Dot followed by number. Could be a fp `.0` or could be a range + num `..0`. Can't tell here. (byte)QuickScanState.Done, // Punct (byte)QuickScanState.Bad, // Dot (DotDot range token, exit so that we handle it in subsequent scanning code) (byte)QuickScanState.Done, // Compound diff --git a/src/Compilers/CSharp/Portable/Parser/SyntaxParser.cs b/src/Compilers/CSharp/Portable/Parser/SyntaxParser.cs index 9f499cc917a49..71b2a9c52bc36 100644 --- a/src/Compilers/CSharp/Portable/Parser/SyntaxParser.cs +++ b/src/Compilers/CSharp/Portable/Parser/SyntaxParser.cs @@ -317,7 +317,7 @@ protected SyntaxToken CurrentToken { get { - return _currentToken ?? (_currentToken = this.FetchCurrentToken()); + return _currentToken ??= this.FetchCurrentToken(); } } diff --git a/src/Compilers/CSharp/Test/Syntax/IncrementalParsing/IncrementalParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/IncrementalParsing/IncrementalParsingTests.cs index fa72f15c6145d..4d4ed05966982 100644 --- a/src/Compilers/CSharp/Test/Syntax/IncrementalParsing/IncrementalParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/IncrementalParsing/IncrementalParsingTests.cs @@ -12,10 +12,11 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Xunit; +using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.CSharp.UnitTests { - public class IncrementalParsingTests : TestBase + public sealed class IncrementalParsingTests(ITestOutputHelper output) : ParsingTests(output) { private CSharpParseOptions GetOptions(string[] defines) { @@ -584,6 +585,267 @@ int LocalFunc() WalkTreeAndVerify(tree.GetCompilationUnitRoot(), fullTree.GetCompilationUnitRoot()); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/74456")] + public void TestCollectionExpressionSpreadVsDeletingTopLevelBrace() + { + const string valueSetterLine = "x[1] = 312;"; + var initialSource = $$""" + public class Program + { + public void M2() + { + if (true) + { + { + if (true) + { + {{valueSetterLine}} + } + } + } + if (true) + { + y = [.. z]; + } + } + } + """; + var initialTree = SyntaxFactory.ParseSyntaxTree(initialSource); + + // Initial code is fully legal and should have no parse errors. + Assert.Empty(initialTree.GetDiagnostics()); + + // Delete '{' (and end of line) before 'values[1] = 312;' + var initialText = initialTree.GetText(); + var valueSetterLinePosition = initialSource.IndexOf(valueSetterLine); + var initialLines = initialText.Lines; + + int valueSetterLineIndex = initialLines.IndexOf(valueSetterLinePosition); + var openBraceLine = initialText.Lines[valueSetterLineIndex - 1]; + Assert.EndsWith("{", openBraceLine.ToString()); + + var withOpenBraceDeletedText = initialText.WithChanges(new TextChange(openBraceLine.SpanIncludingLineBreak, "")); + var withOpenBraceDeletedTree = initialTree.WithChangedText(withOpenBraceDeletedText); + + // Deletion of the open brace causes the method body to close early with the close brace before the `if` + // statement. This will lead to a ton of cascading errors for what follows. In particular, the `[.. values]` + // will be parsed as a very broken attribute on an incomplete member. + { + UsingTree(withOpenBraceDeletedTree, + // (13,9): error CS1519: Invalid token 'if' in class, record, struct, or interface member declaration + // if (true) + Diagnostic(ErrorCode.ERR_InvalidMemberDecl, "if").WithArguments("if").WithLocation(13, 9), + // (13,13): error CS1031: Type expected + // if (true) + Diagnostic(ErrorCode.ERR_TypeExpected, "true").WithLocation(13, 13), + // (13,13): error CS8124: Tuple must contain at least two elements. + // if (true) + Diagnostic(ErrorCode.ERR_TupleTooFewElements, "true").WithLocation(13, 13), + // (13,13): error CS1026: ) expected + // if (true) + Diagnostic(ErrorCode.ERR_CloseParenExpected, "true").WithLocation(13, 13), + // (13,13): error CS1519: Invalid token 'true' in class, record, struct, or interface member declaration + // if (true) + Diagnostic(ErrorCode.ERR_InvalidMemberDecl, "true").WithArguments("true").WithLocation(13, 13), + // (15,15): error CS1519: Invalid token '=' in class, record, struct, or interface member declaration + // y = [.. z]; + Diagnostic(ErrorCode.ERR_InvalidMemberDecl, "=").WithArguments("=").WithLocation(15, 15), + // (15,15): error CS1519: Invalid token '=' in class, record, struct, or interface member declaration + // y = [.. z]; + Diagnostic(ErrorCode.ERR_InvalidMemberDecl, "=").WithArguments("=").WithLocation(15, 15), + // (15,18): error CS1001: Identifier expected + // y = [.. z]; + Diagnostic(ErrorCode.ERR_IdentifierExpected, ".").WithLocation(15, 18), + // (15,19): error CS1001: Identifier expected + // y = [.. z]; + Diagnostic(ErrorCode.ERR_IdentifierExpected, ".").WithLocation(15, 19), + // (15,23): error CS1519: Invalid token ';' in class, record, struct, or interface member declaration + // y = [.. z]; + Diagnostic(ErrorCode.ERR_InvalidMemberDecl, ";").WithArguments(";").WithLocation(15, 23), + // (17,5): error CS1022: Type or namespace definition, or end-of-file expected + // } + Diagnostic(ErrorCode.ERR_EOFExpected, "}").WithLocation(17, 5), + // (18,1): error CS1022: Type or namespace definition, or end-of-file expected + // } + Diagnostic(ErrorCode.ERR_EOFExpected, "}").WithLocation(18, 1)); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.ClassDeclaration); + { + N(SyntaxKind.PublicKeyword); + N(SyntaxKind.ClassKeyword); + N(SyntaxKind.IdentifierToken, "Program"); + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PublicKeyword); + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken, "M2"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.IfStatement); + { + N(SyntaxKind.IfKeyword); + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.TrueLiteralExpression); + { + N(SyntaxKind.TrueKeyword); + } + N(SyntaxKind.CloseParenToken); + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.IfStatement); + { + N(SyntaxKind.IfKeyword); + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.TrueLiteralExpression); + { + N(SyntaxKind.TrueKeyword); + } + N(SyntaxKind.CloseParenToken); + N(SyntaxKind.ExpressionStatement); + { + N(SyntaxKind.SimpleAssignmentExpression); + { + N(SyntaxKind.ElementAccessExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "x"); + } + N(SyntaxKind.BracketedArgumentList); + { + N(SyntaxKind.OpenBracketToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.NumericLiteralExpression); + { + N(SyntaxKind.NumericLiteralToken, "1"); + } + } + N(SyntaxKind.CloseBracketToken); + } + } + N(SyntaxKind.EqualsToken); + N(SyntaxKind.NumericLiteralExpression); + { + N(SyntaxKind.NumericLiteralToken, "312"); + } + } + N(SyntaxKind.SemicolonToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + } + // Here is where we go off the rails. This corresponds to the `if (true) ...` part after the method + N(SyntaxKind.IncompleteMember); + { + N(SyntaxKind.TupleType); + { + N(SyntaxKind.OpenParenToken); + M(SyntaxKind.TupleElement); + { + M(SyntaxKind.IdentifierName); + { + M(SyntaxKind.IdentifierToken); + } + } + M(SyntaxKind.CommaToken); + M(SyntaxKind.TupleElement); + { + M(SyntaxKind.IdentifierName); + { + M(SyntaxKind.IdentifierToken); + } + } + M(SyntaxKind.CloseParenToken); + } + } + // this corresponds to 'y' in 'y = [.. z];' + N(SyntaxKind.IncompleteMember); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "y"); + } + } + // This corresponds to `[.. z]` which parser thinks is an attribute with an invalid dotted name. + N(SyntaxKind.IncompleteMember); + { + N(SyntaxKind.AttributeList); + { + N(SyntaxKind.OpenBracketToken); + N(SyntaxKind.Attribute); + { + N(SyntaxKind.QualifiedName); + { + N(SyntaxKind.QualifiedName); + { + M(SyntaxKind.IdentifierName); + { + M(SyntaxKind.IdentifierToken); + } + N(SyntaxKind.DotToken); + M(SyntaxKind.IdentifierName); + { + M(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.DotToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "z"); + } + } + } + N(SyntaxKind.CloseBracketToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + // Now delete '}' after 'values[1] = 312;'. This should result in no diagnostics. + // + // Note: because we deleted the end of line after the '{', the line that is now on the line where `values...` was + // will be the line that was originally after it (the } line). + var withOpenBraceDeletedLines = withOpenBraceDeletedText.Lines; + var closeBraceLine = withOpenBraceDeletedLines[valueSetterLineIndex]; + Assert.EndsWith("}", closeBraceLine.ToString()); + var withCloseBraceDeletedText = withOpenBraceDeletedText.WithChanges(new TextChange(closeBraceLine.SpanIncludingLineBreak, "")); + var withCloseBraceDeletedTree = withOpenBraceDeletedTree.WithChangedText(withCloseBraceDeletedText); + + Assert.Empty(withCloseBraceDeletedTree.GetDiagnostics()); + + var fullTree = SyntaxFactory.ParseSyntaxTree(withCloseBraceDeletedText.ToString()); + Assert.Empty(fullTree.GetDiagnostics()); + + WalkTreeAndVerify(withCloseBraceDeletedTree.GetCompilationUnitRoot(), fullTree.GetCompilationUnitRoot()); + } + #region "Regression" #if false diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/CollectionExpressionParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/CollectionExpressionParsingTests.cs index 46b1fc99900ba..417f732a88673 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/CollectionExpressionParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/CollectionExpressionParsingTests.cs @@ -6992,10 +6992,7 @@ public void TestError5() UsingExpression("[...e]", // (1,2): error CS8635: Unexpected character sequence '...' // [...e] - Diagnostic(ErrorCode.ERR_TripleDotNotAllowed, "").WithLocation(1, 2), - // (1,4): error CS1525: Invalid expression term '.' - // [...e] - Diagnostic(ErrorCode.ERR_InvalidExprTerm, ".").WithArguments(".").WithLocation(1, 4)); + Diagnostic(ErrorCode.ERR_TripleDotNotAllowed, "").WithLocation(1, 2)); N(SyntaxKind.CollectionExpression); { @@ -7003,17 +7000,9 @@ public void TestError5() N(SyntaxKind.SpreadElement); { N(SyntaxKind.DotDotToken); - N(SyntaxKind.SimpleMemberAccessExpression); + N(SyntaxKind.IdentifierName); { - M(SyntaxKind.IdentifierName); - { - M(SyntaxKind.IdentifierToken); - } - N(SyntaxKind.DotToken); - N(SyntaxKind.IdentifierName); - { - N(SyntaxKind.IdentifierToken, "e"); - } + N(SyntaxKind.IdentifierToken, "e"); } } N(SyntaxKind.CloseBracketToken); diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs index 35c676b2131ec..268d4e0ce5d5d 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs @@ -5284,7 +5284,7 @@ public void IndexExpression() public void RangeExpression_ThreeDots() { UsingExpression("1...2", - // (1,2): error CS8401: Unexpected character sequence '...' + // (1,2): error CS8635: Unexpected character sequence '...' // 1...2 Diagnostic(ErrorCode.ERR_TripleDotNotAllowed, "").WithLocation(1, 2)); @@ -5297,7 +5297,7 @@ public void RangeExpression_ThreeDots() N(SyntaxKind.DotDotToken); N(SyntaxKind.NumericLiteralExpression); { - N(SyntaxKind.NumericLiteralToken, ".2"); + N(SyntaxKind.NumericLiteralToken, "2"); } } EOF(); diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/MemberDeclarationParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/MemberDeclarationParsingTests.cs index a98c94c918dd3..863aad136bf7c 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/MemberDeclarationParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/MemberDeclarationParsingTests.cs @@ -4118,8 +4118,7 @@ public void OperatorDeclaration_ExplicitImplementation_21() UsingDeclaration("public int I..operator +(int x) => x;", options: options.WithLanguageVersion(LanguageVersion.Preview), // (1,14): error CS1001: Identifier expected // public int I..operator +(int x) => x; - Diagnostic(ErrorCode.ERR_IdentifierExpected, ".operato").WithLocation(1, 14) - ); + Diagnostic(ErrorCode.ERR_IdentifierExpected, ".").WithLocation(1, 14)); N(SyntaxKind.OperatorDeclaration); { @@ -5964,8 +5963,7 @@ public void OperatorDeclaration_ExplicitImplementation_43() UsingDeclaration("int I..operator +(int x) => x;", options: options.WithLanguageVersion(LanguageVersion.Preview), // (1,7): error CS1001: Identifier expected // int I..operator +(int x) => x; - Diagnostic(ErrorCode.ERR_IdentifierExpected, ".operato").WithLocation(1, 7) - ); + Diagnostic(ErrorCode.ERR_IdentifierExpected, ".").WithLocation(1, 7)); N(SyntaxKind.OperatorDeclaration); { @@ -6088,8 +6086,7 @@ public void OperatorDeclaration_ExplicitImplementation_45() UsingDeclaration("int N.I..operator +(int x) => x;", options: options.WithLanguageVersion(LanguageVersion.Preview), // (1,9): error CS1001: Identifier expected // int N.I..operator +(int x) => x; - Diagnostic(ErrorCode.ERR_IdentifierExpected, ".operato").WithLocation(1, 9) - ); + Diagnostic(ErrorCode.ERR_IdentifierExpected, ".").WithLocation(1, 9)); N(SyntaxKind.OperatorDeclaration); { @@ -7819,8 +7816,7 @@ public void ConversionDeclaration_ExplicitImplementation_21() UsingDeclaration("explicit I..operator int(int x) => x;", options: options.WithLanguageVersion(LanguageVersion.Preview), // (1,12): error CS1001: Identifier expected // explicit I..operator int(int x) => x; - Diagnostic(ErrorCode.ERR_IdentifierExpected, ".operato").WithLocation(1, 12) - ); + Diagnostic(ErrorCode.ERR_IdentifierExpected, ".").WithLocation(1, 12)); N(SyntaxKind.ConversionOperatorDeclaration); { @@ -8551,8 +8547,7 @@ public void ConversionDeclaration_ExplicitImplementation_34() UsingDeclaration("explicit N.I..operator int(int x) => x;", options: options.WithLanguageVersion(LanguageVersion.Preview), // (1,14): error CS1001: Identifier expected // explicit N.I..operator int(int x) => x; - Diagnostic(ErrorCode.ERR_IdentifierExpected, ".operato").WithLocation(1, 14) - ); + Diagnostic(ErrorCode.ERR_IdentifierExpected, ".").WithLocation(1, 14)); N(SyntaxKind.ConversionOperatorDeclaration); { diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/PatternParsingTests_ListPatterns.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/PatternParsingTests_ListPatterns.cs index 422e1f3cb456f..e3c5ee9b98ed6 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/PatternParsingTests_ListPatterns.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/PatternParsingTests_ListPatterns.cs @@ -530,8 +530,7 @@ public void SlicePattern_04() UsingExpression(@"c is ....", // (1,6): error CS8635: Unexpected character sequence '...' // c is .... - Diagnostic(ErrorCode.ERR_TripleDotNotAllowed, "").WithLocation(1, 6) - ); + Diagnostic(ErrorCode.ERR_TripleDotNotAllowed, "").WithLocation(1, 6)); N(SyntaxKind.IsPatternExpression); { @@ -767,9 +766,9 @@ public void SlicePattern_10() public void SlicePattern_11() { UsingExpression(@"c is [var x ..]", - // (1,13): error CS1003: Syntax error, ',' expected - // c is [var x ..] - Diagnostic(ErrorCode.ERR_SyntaxError, "..").WithArguments(",").WithLocation(1, 13)); + // (1,13): error CS1003: Syntax error, ',' expected + // c is [var x ..] + Diagnostic(ErrorCode.ERR_SyntaxError, ".").WithArguments(",").WithLocation(1, 13)); N(SyntaxKind.IsPatternExpression); { @@ -837,7 +836,7 @@ public void SlicePattern_13() UsingExpression(@"c is [[]..]", // (1,9): error CS1003: Syntax error, ',' expected // c is [[]..] - Diagnostic(ErrorCode.ERR_SyntaxError, "..").WithArguments(",").WithLocation(1, 9)); + Diagnostic(ErrorCode.ERR_SyntaxError, ".").WithArguments(",").WithLocation(1, 9)); N(SyntaxKind.IsPatternExpression); { @@ -871,7 +870,7 @@ public void SlicePattern_14() UsingExpression(@"c is not p ..", // (1,13): error CS1001: Identifier expected // c is not p .. - Diagnostic(ErrorCode.ERR_IdentifierExpected, "").WithLocation(1, 13), + Diagnostic(ErrorCode.ERR_IdentifierExpected, ".").WithLocation(1, 13), // (1,14): error CS1001: Identifier expected // c is not p .. Diagnostic(ErrorCode.ERR_IdentifierExpected, "").WithLocation(1, 14)); diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/RangeExpressionParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/RangeExpressionParsingTests.cs new file mode 100644 index 0000000000000..3a3fc86d16063 --- /dev/null +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/RangeExpressionParsingTests.cs @@ -0,0 +1,318 @@ +// 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. + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests; + +public sealed class RangeExpressionParsingTests(ITestOutputHelper output) + : ParsingTests(output) +{ + [Fact] + public void CastingRangeExpressionWithoutStartOrEnd() + { + UsingExpression("(int).."); + + N(SyntaxKind.CastExpression); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.IntKeyword); + } + N(SyntaxKind.CloseParenToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + } + } + EOF(); + } + + [Fact] + public void CastingRangeExpressionWithoutStart() + { + UsingExpression("(int)..0"); + + N(SyntaxKind.CastExpression); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.IntKeyword); + } + N(SyntaxKind.CloseParenToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + N(SyntaxKind.NumericLiteralExpression); + { + N(SyntaxKind.NumericLiteralToken, "0"); + } + } + } + EOF(); + } + + [Fact] + public void ConditionalExpressionWithEmptyRangeForWhenTrue() + { + UsingExpression("a ? .. : b"); + + N(SyntaxKind.ConditionalExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "a"); + } + N(SyntaxKind.QuestionToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + } + N(SyntaxKind.ColonToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "b"); + } + } + EOF(); + } + + [Fact] + public void ConditionalExpressionWithEmptyRangeForWhenFalse() + { + UsingExpression("a ? b : .."); + + N(SyntaxKind.ConditionalExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "a"); + } + N(SyntaxKind.QuestionToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "b"); + } + N(SyntaxKind.ColonToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + } + } + EOF(); + } + + [Fact] + public void ConditionalExpressionWithEmptyRangeForWhenTrueAndWhenFalse() + { + UsingExpression("a ? .. : .."); + + N(SyntaxKind.ConditionalExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "a"); + } + N(SyntaxKind.QuestionToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + } + N(SyntaxKind.ColonToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + } + } + EOF(); + } + + [Fact] + public void ConditionalExpressionWithEmptyStartRangeForWhenTrue() + { + UsingExpression("a ? ..b : c"); + + N(SyntaxKind.ConditionalExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "a"); + } + N(SyntaxKind.QuestionToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "b"); + } + } + N(SyntaxKind.ColonToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "c"); + } + } + EOF(); + } + + [Fact] + public void ConditionalExpressionWithEmptyStartRangeForWhenFalse() + { + UsingExpression("a ? b : ..c"); + + N(SyntaxKind.ConditionalExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "a"); + } + N(SyntaxKind.QuestionToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "b"); + } + N(SyntaxKind.ColonToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "c"); + } + } + } + EOF(); + } + + [Fact] + public void ConditionalExpressionWithEmptyStartRangeForWhenTrueAndFalse() + { + UsingExpression("a ? ..b : ..c"); + + N(SyntaxKind.ConditionalExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "a"); + } + N(SyntaxKind.QuestionToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "b"); + } + } + N(SyntaxKind.ColonToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "c"); + } + } + } + EOF(); + } + + [Fact] + public void CastingRangeExpressionInPattern1() + { + UsingExpression("x is (int).."); + + N(SyntaxKind.IsPatternExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "x"); + } + N(SyntaxKind.IsKeyword); + N(SyntaxKind.ConstantPattern); + { + N(SyntaxKind.CastExpression); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.IntKeyword); + } + N(SyntaxKind.CloseParenToken); + N(SyntaxKind.RangeExpression); + { + N(SyntaxKind.DotDotToken); + } + } + } + } + EOF(); + } + + [Fact] + public void CastingRangeExpressionInPattern2() + { + UsingExpression("x is (int).", + // (1,1): error CS1073: Unexpected token '.' + // x is (int). + Diagnostic(ErrorCode.ERR_UnexpectedToken, "x is (int)").WithArguments(".").WithLocation(1, 1)); + + N(SyntaxKind.IsPatternExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "x"); + } + N(SyntaxKind.IsKeyword); + N(SyntaxKind.ParenthesizedPattern); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.TypePattern); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.IntKeyword); + } + } + N(SyntaxKind.CloseParenToken); + } + } + EOF(); + } + + [Fact] + public void CastingRangeExpressionInPattern3() + { + UsingExpression("x is (int).Length", + // (1,1): error CS1073: Unexpected token '.' + // x is (int).Length + Diagnostic(ErrorCode.ERR_UnexpectedToken, "x is (int)").WithArguments(".").WithLocation(1, 1)); + + N(SyntaxKind.IsPatternExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "x"); + } + N(SyntaxKind.IsKeyword); + N(SyntaxKind.ParenthesizedPattern); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.TypePattern); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.IntKeyword); + } + } + N(SyntaxKind.CloseParenToken); + } + } + EOF(); + } +}