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
base: main
Are you sure you want to change the base?
Improve parser recovery around nullable types in patterns #72805
Conversation
41d70bf
to
c058b16
Compare
src/Compilers/CSharp/Test/Syntax/Parsing/NullableParsingTests.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Syntax/Parsing/NullableParsingTests.cs
Outdated
Show resolved
Hide resolved
@@ -1774,5 +1756,379 @@ public void CreateNullableArray_07() | |||
} | |||
EOF(); | |||
} | |||
|
|||
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72720")] | |||
public void DisjunctivePattern_NullableType1() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i appreciate all the new tests.
src/Compilers/CSharp/Test/Syntax/Parsing/PatternParsingTests_ListPatterns.cs
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Syntax/Parsing/SwitchExpressionParsingTests.cs
Show resolved
Hide resolved
Done with pass. Overall approve of the high level idea. Def wary of the actual code change (i don't understand it yet), and def think we need to beef up tests to ensure that things that are now legal to parse give proper diagnostics. |
// Since nullable types (no matter reference of value types) are not valid in patterns | ||
// by default we are biased towards the first option and consider case 2 only for error recovery purposes | ||
// (if we parse here as nullable type pattern an error will be reported during binding). | ||
// This condition checks for simple cases, where we better use option 2 and parse a nullable-typed pattern |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this comment was great.
Ok. Pushed. I'm also ok changing the await-handling and updating the
tests accordingly. Are you ok with me doing that, or would you prefer to
do that?
|
Go ahead. Just add a simple comment to changed tests, so it is obvious, that it is an intentional "degradation" |
Will do. |
@@ -7015,20 +7015,20 @@ private enum ParseTypeMode | |||
FirstElementOfPossibleTupleLiteral, | |||
} | |||
|
|||
private TypeSyntax ParseType(ParseTypeMode mode = ParseTypeMode.Normal) | |||
private TypeSyntax ParseType(ParseTypeMode mode = ParseTypeMode.Normal, bool inSwitchArmPattern = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whenIsKeyword
got renamed to inSwitchArmPattern
to try to be clearer. We pass 'true' in here when parsign the pattern for a switch-statement or switch-expression pattern. And it controls how we view 'where' and a few other error recovery rules (including new ones in this pr).
@@ -431,7 +438,7 @@ private CSharpSyntaxNode ParseExpressionOrPatternForSwitchStatement() | |||
{ | |||
var savedState = _termState; | |||
_termState |= TerminatorState.IsExpressionOrPatternInCaseLabelOfSwitchStatement; | |||
var pattern = ParsePattern(Precedence.Conditional, whenIsKeyword: true); | |||
var pattern = ParsePattern(Precedence.Conditional, inSwitchArmPattern: true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one of hte two locations where 'true' is passed in. Here we're parsing case clauses for switch-statements.
@@ -589,7 +596,7 @@ SeparatedSyntaxList<SwitchExpressionArmSyntax> parseSwitchExpressionArms() | |||
|
|||
var savedState = _termState; | |||
_termState |= TerminatorState.IsPatternInSwitchExpressionArm; | |||
var pattern = ParsePattern(Precedence.Coalescing, whenIsKeyword: true); | |||
var pattern = ParsePattern(Precedence.Coalescing, inSwitchArmPattern: true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one of hte two locations where 'true' is passed in. Here we're parsing the arms for switch-expressions.
lgtm. @dotnet/roslyn-compiler for reviews please. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done review pass. I didn't get any further than the comments that spelled out invalid assumptions for parsing behavior.
{ | ||
// If we're in a cast statement/expression arm, then we have: | ||
// | ||
// `switch { case X ? y` or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumption is not correct. The following is valid code:
switch(i3) {
case b ? i1 : i2: break;
}
This comment needs to be corrected, and we should re-examine this approach to ensure that we're not breaking anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "why on Earth this is legal" question should have probably been asked N years ago when switch statements were designed) I was sure that conditional expressions in case labels aren't semantically legal and thus there is no difference whether this is parsed as a conditional or a nullable pattern + label statement. There is already a test in the codebase for that case and I initially wrote a comment explaining how these 2 parse strategies both lead to a compiler error thus are identical for our purposes, had to revert that change. And I'm somewhat surprised that only a very few new test cases now are back to bad state after removing this assumption, so I don't think we should do additional work to restore them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "why on Earth this is legal" question should have probably been asked N years ago when switch statements were designed)
So, this is legal because cases allow for patterns. And one sort of pattern is the 'constant pattern' (that's how x is 0
works for example). So these places allow for any sort of constant expression (including conditional expressions). Note: for conditional expressions to be constant, their condition itself must be constant. But that can often normally happen, esp. in generated code, or code referencing constants that are conditionally compiled in.
// | ||
// `switch { case X ? y` or | ||
// | ||
// e switch { X ? y ` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, this assumption is closer to correct, but isn't always:
_ = i3 switch {
(b ? i1 : i2) => 3
};
You do need the parentheses to force parse in this manner, but I'm concerned that we'll still end up with bad behavior here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this requires parentheses, it wasn't affected even with the wrong assumption. Added a test for this case
…m/DoctorKrolic/roslyn into nullable-type-in-pattern-recovery
UsingExpression(""" | ||
obj switch | ||
{ | ||
(flag ? a : b) => 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you have a test for switch statements as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are already in the codebase. See commit 3a6f3bd, I've changed one of those tests back to what it was. The parenthesized case is exactly below it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here they are in main
:
roslyn/src/Compilers/CSharp/Test/Syntax/Parsing/PatternParsingTests.cs
Lines 12585 to 12690 in 93d19c2
[Fact] | |
public void ConditionalAsConstantPatternInSwitchStatement() | |
{ | |
UsingStatement(@"switch(a) { case a?x:y: break; }", | |
TestOptions.RegularWithPatternCombinators | |
); | |
N(SyntaxKind.SwitchStatement); | |
{ | |
N(SyntaxKind.SwitchKeyword); | |
N(SyntaxKind.OpenParenToken); | |
N(SyntaxKind.IdentifierName); | |
{ | |
N(SyntaxKind.IdentifierToken, "a"); | |
} | |
N(SyntaxKind.CloseParenToken); | |
N(SyntaxKind.OpenBraceToken); | |
N(SyntaxKind.SwitchSection); | |
{ | |
N(SyntaxKind.CaseSwitchLabel); | |
{ | |
N(SyntaxKind.CaseKeyword); | |
N(SyntaxKind.ConditionalExpression); | |
{ | |
N(SyntaxKind.IdentifierName); | |
{ | |
N(SyntaxKind.IdentifierToken, "a"); | |
} | |
N(SyntaxKind.QuestionToken); | |
N(SyntaxKind.IdentifierName); | |
{ | |
N(SyntaxKind.IdentifierToken, "x"); | |
} | |
N(SyntaxKind.ColonToken); | |
N(SyntaxKind.IdentifierName); | |
{ | |
N(SyntaxKind.IdentifierToken, "y"); | |
} | |
} | |
N(SyntaxKind.ColonToken); | |
} | |
N(SyntaxKind.BreakStatement); | |
{ | |
N(SyntaxKind.BreakKeyword); | |
N(SyntaxKind.SemicolonToken); | |
} | |
} | |
N(SyntaxKind.CloseBraceToken); | |
} | |
EOF(); | |
} | |
[Fact] | |
public void ConditionalAsConstantPatternInSwitchStatement_Parenthesized() | |
{ | |
UsingStatement(@"switch(a) { case (a?x:y): break; }", | |
TestOptions.RegularWithPatternCombinators | |
); | |
N(SyntaxKind.SwitchStatement); | |
{ | |
N(SyntaxKind.SwitchKeyword); | |
N(SyntaxKind.OpenParenToken); | |
N(SyntaxKind.IdentifierName); | |
{ | |
N(SyntaxKind.IdentifierToken, "a"); | |
} | |
N(SyntaxKind.CloseParenToken); | |
N(SyntaxKind.OpenBraceToken); | |
N(SyntaxKind.SwitchSection); | |
{ | |
N(SyntaxKind.CaseSwitchLabel); | |
{ | |
N(SyntaxKind.CaseKeyword); | |
N(SyntaxKind.ParenthesizedExpression); | |
{ | |
N(SyntaxKind.OpenParenToken); | |
N(SyntaxKind.ConditionalExpression); | |
{ | |
N(SyntaxKind.IdentifierName); | |
{ | |
N(SyntaxKind.IdentifierToken, "a"); | |
} | |
N(SyntaxKind.QuestionToken); | |
N(SyntaxKind.IdentifierName); | |
{ | |
N(SyntaxKind.IdentifierToken, "x"); | |
} | |
N(SyntaxKind.ColonToken); | |
N(SyntaxKind.IdentifierName); | |
{ | |
N(SyntaxKind.IdentifierToken, "y"); | |
} | |
} | |
N(SyntaxKind.CloseParenToken); | |
} | |
N(SyntaxKind.ColonToken); | |
} | |
N(SyntaxKind.BreakStatement); | |
{ | |
N(SyntaxKind.BreakKeyword); | |
N(SyntaxKind.SemicolonToken); | |
} | |
} | |
N(SyntaxKind.CloseBraceToken); | |
} | |
EOF(); | |
} |
Closes: #72720