Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve parser recovery around nullable types in patterns #72805

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c058b16
Improve parser recovery around nullable types in patterns
DoctorKrolic Mar 29, 2024
af8391d
Add test
DoctorKrolic Mar 29, 2024
233adf0
Replace with more specific checks
DoctorKrolic Mar 29, 2024
21844c3
Already covered by `IsLiteral` check
DoctorKrolic Mar 29, 2024
7894262
Remove incorrect cases
DoctorKrolic Mar 29, 2024
db65d50
Fix existing semantic tests
DoctorKrolic Mar 29, 2024
dc7410f
Fix test
DoctorKrolic Mar 30, 2024
5d413b7
Simplify
DoctorKrolic Mar 30, 2024
212c0ae
Add binding tests
DoctorKrolic Mar 30, 2024
5c8eb47
Add array binding tests
DoctorKrolic Mar 30, 2024
bedc825
Fix test
DoctorKrolic Mar 30, 2024
568985b
Add `;` to the list of tokens, which end pattern
DoctorKrolic Mar 30, 2024
59a4e77
Fix `await` parsing
DoctorKrolic Mar 30, 2024
1d92ba9
Add more reference type tests
DoctorKrolic Apr 1, 2024
45e87df
Merge branch 'dotnet:main' into nullable-type-in-pattern-recovery
DoctorKrolic Apr 2, 2024
382b1c0
Add tests where `await` is a pattern variable
DoctorKrolic Apr 2, 2024
e4869d9
Add tests from main
DoctorKrolic Apr 2, 2024
00eaf58
Merge branch 'main' into nullable-type-in-pattern-recovery
DoctorKrolic Apr 2, 2024
a8d1fe5
Add more tests with missing comma in list pattern followed by a literal
DoctorKrolic Apr 13, 2024
fd940ed
Explain my life choices
DoctorKrolic Apr 13, 2024
4b7079f
Add more tests around predefined types
DoctorKrolic Apr 13, 2024
4c404b8
Use single flag
DoctorKrolic May 5, 2024
bd87f6f
Better comment
DoctorKrolic May 5, 2024
b5e5fd9
Merge remote-tracking branch 'upstream/main' into nullable-type-in-pa…
CyrusNajmabadi May 5, 2024
7e7203d
break into pieces
CyrusNajmabadi May 5, 2024
bd74493
Await handling
CyrusNajmabadi May 5, 2024
2c90fdb
rename
CyrusNajmabadi May 5, 2024
80410e9
lint
CyrusNajmabadi May 5, 2024
006b7b9
Add test with conditional expression in switch expression
DoctorKrolic May 7, 2024
5dcc50a
Merge branch 'nullable-type-in-pattern-recovery' of https://github.co…
DoctorKrolic May 7, 2024
3a6f3bd
Remove wrong assumption
DoctorKrolic May 7, 2024
3c74dfd
Typo
DoctorKrolic May 7, 2024
19ee361
Fix semantic tests
DoctorKrolic May 7, 2024
13bf31c
Fix test
DoctorKrolic May 7, 2024
7d105d8
Grammar
DoctorKrolic Jun 13, 2024
58ad159
Dot
DoctorKrolic Jun 13, 2024
57b925e
Merge branch 'dotnet:main' into nullable-type-in-pattern-recovery
DoctorKrolic Jun 13, 2024
9db390d
Revert
DoctorKrolic Jun 13, 2024
7c39565
Fix parsing af an async lambda in a conditional expression
DoctorKrolic Jun 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions src/Compilers/CSharp/Portable/Parser/LanguageParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6916,20 +6916,20 @@ private enum ParseTypeMode
FirstElementOfPossibleTupleLiteral,
}

private TypeSyntax ParseType(ParseTypeMode mode = ParseTypeMode.Normal)
private TypeSyntax ParseType(ParseTypeMode mode = ParseTypeMode.Normal, bool whenIsKeyword = false)
DoctorKrolic marked this conversation as resolved.
Show resolved Hide resolved
{
if (this.CurrentToken.Kind == SyntaxKind.RefKeyword)
{
return _syntaxFactory.RefType(
this.EatToken(),
this.CurrentToken.Kind == SyntaxKind.ReadOnlyKeyword ? this.EatToken() : null,
ParseTypeCore(ParseTypeMode.AfterRef));
ParseTypeCore(ParseTypeMode.AfterRef, whenIsKeyword));
}

return ParseTypeCore(mode);
return ParseTypeCore(mode, whenIsKeyword);
}

private TypeSyntax ParseTypeCore(ParseTypeMode mode)
private TypeSyntax ParseTypeCore(ParseTypeMode mode, bool whenIsKeyword)
{
NameOptions nameOptions;
switch (mode)
Expand Down Expand Up @@ -6970,7 +6970,7 @@ private TypeSyntax ParseTypeCore(ParseTypeMode mode)
{
case SyntaxKind.QuestionToken when canBeNullableType():
{
var question = EatNullableQualifierIfApplicable(mode);
var question = EatNullableQualifierIfApplicable(mode, whenIsKeyword);
if (question != null)
{
type = _syntaxFactory.NullableType(type, question);
Expand Down Expand Up @@ -7043,7 +7043,7 @@ bool canBeNullableType()
return type;
}

private SyntaxToken EatNullableQualifierIfApplicable(ParseTypeMode mode)
private SyntaxToken EatNullableQualifierIfApplicable(ParseTypeMode mode, bool whenIsKeyword)
{
Debug.Assert(this.CurrentToken.Kind == SyntaxKind.QuestionToken);
using var resetPoint = this.GetDisposableResetPoint(resetOnDispose: false);
Expand All @@ -7065,12 +7065,49 @@ bool canFollowNullableType()
case ParseTypeMode.AfterIs:
case ParseTypeMode.DefinitePattern:
case ParseTypeMode.AsExpression:
// These contexts might be a type that is at the end of an expression. In these contexts we only
// permit the nullable qualifier if it is followed by a token that could not start an
// expression, because for backward compatibility we want to consider a `?` token as part of the
// `?:` operator if possible.
//
// Similarly, if we have `T?[]` we do want to treat that as an array of nullables (following
// These contexts might be a type that is at the end of an expression.
// For backward compatibility we want to consider a `?` token as part of the `?:` operator if possible.
DoctorKrolic marked this conversation as resolved.
Show resolved Hide resolved
// However, if current token is an identifier, it might be beneficial to allow `?`
DoctorKrolic marked this conversation as resolved.
Show resolved Hide resolved
// and treat this identifier as a designation. Nullable types in patterns are semantically invalid,
// so we will get a nice error about that during binding.
if (this.CurrentToken.Kind == SyntaxKind.IdentifierToken)
{
// Given that we are deciding whether we take `?:` operator or not while looking at state after `?`,
// our focus is on `:` token as well. But `:` have special meaning in context of switch statements
// and expressions (since `:` in switch expressions is recovered to `=>`). So consider the following case:
// `case T? t:`. On its own `T ? t :` can be treated as a conditional expression with condition `T`,
// 'whenTrue' `t` and 'whenFalse' not yet parsed. But it is way better to take `:` as case label end and
// thus parse label condition as a declaration pattern `T? t`. `whenIsKeyword` flag can conveniently tell
// whether we are in a 'top-level' state of a label, so if condition get parenthesized like `case (a ? b : c):`
// we don't take the first `:` as label end and parse label condition as a parenthesized conditional expression
if ((_termState.HasFlag(TerminatorState.IsExpressionOrPatternInCaseLabelOfSwitchStatement) ||
DoctorKrolic marked this conversation as resolved.
Show resolved Hide resolved
_termState.HasFlag(TerminatorState.IsPatternInSwitchExpressionArm)) && whenIsKeyword)
{
return true;
}

using var _ = GetDisposableResetPoint(resetOnDispose: true);
this.EatToken();

// If token after identifier starts an expression it is probably an error case, e.g. missing a `,` in a pattern
DoctorKrolic marked this conversation as resolved.
Show resolved Hide resolved
if (CanStartExpression())
{
return true;
}

// These token either 100% end a pattern or start a new one (again, in cases with missing `,` and so on).
// Note, that some cases of 'token starts a new pattern' are handled by condition above,
// e.g. starting of type patterns
return this.CurrentToken.Kind is SyntaxKind.OpenParenToken or SyntaxKind.CloseParenToken
DoctorKrolic marked this conversation as resolved.
Show resolved Hide resolved
or SyntaxKind.OpenBraceToken or SyntaxKind.CloseBraceToken
or SyntaxKind.OpenBracketToken or SyntaxKind.CloseBracketToken
or SyntaxKind.CommaToken
or SyntaxKind.EndOfFileToken;
}

// If nothing from above worked permit the nullable qualifier
// if it is followed by a token that could not start an expression
// If we have `T?[]` we do want to treat that as an array of nullables (following
// existing parsing), not a conditional that returns a list.
return !CanStartExpression() || this.CurrentToken.Kind is SyntaxKind.OpenBracketToken;
case ParseTypeMode.NewExpression:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ private PatternSyntax ParsePrimaryPattern(Precedence precedence, bool afterIs, b
TypeSyntax? type = null;
if (LooksLikeTypeOfPattern())
{
type = this.ParseType(afterIs ? ParseTypeMode.AfterIs : ParseTypeMode.DefinitePattern);
type = this.ParseType(afterIs ? ParseTypeMode.AfterIs : ParseTypeMode.DefinitePattern, whenIsKeyword);
if (type.IsMissing || !CanTokenFollowTypeInPattern(precedence))
{
// either it is not shaped like a type, or it is a constant expression.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#nullable disable

using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -134,45 +135,37 @@ public void NullableTypeTest_02()
EOF();
}

[Fact]
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72720")]
public void NullableTypeTest_03()
{
UsingStatement("if (e is int? x) {}",
// (1,16): error CS1003: Syntax error, ':' expected
// if (e is int? x) {}
Diagnostic(ErrorCode.ERR_SyntaxError, ")").WithArguments(":").WithLocation(1, 16),
// (1,16): error CS1525: Invalid expression term ')'
// if (e is int? x) {}
Diagnostic(ErrorCode.ERR_InvalidExprTerm, ")").WithArguments(")").WithLocation(1, 16)
);
UsingStatement("if (e is int? x) {}");
DoctorKrolic marked this conversation as resolved.
Show resolved Hide resolved

N(SyntaxKind.IfStatement);
{
N(SyntaxKind.IfKeyword);
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.ConditionalExpression);
N(SyntaxKind.IsPatternExpression);
{
N(SyntaxKind.IsExpression);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierName);
N(SyntaxKind.IdentifierToken, "e");
}
N(SyntaxKind.IsKeyword);
N(SyntaxKind.DeclarationPattern);
{
N(SyntaxKind.NullableType);
{
N(SyntaxKind.IdentifierToken, "e");
N(SyntaxKind.PredefinedType);
{
N(SyntaxKind.IntKeyword);
}
N(SyntaxKind.QuestionToken);
}
N(SyntaxKind.IsKeyword);
N(SyntaxKind.PredefinedType);
N(SyntaxKind.SingleVariableDesignation);
{
N(SyntaxKind.IntKeyword);
N(SyntaxKind.IdentifierToken, "x");
}
}
N(SyntaxKind.QuestionToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "x");
}
M(SyntaxKind.ColonToken);
M(SyntaxKind.IdentifierName);
{
M(SyntaxKind.IdentifierToken);
}
}
N(SyntaxKind.CloseParenToken);
N(SyntaxKind.Block);
Expand Down
Loading
Loading