-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Refine parsing of potentially ambiguous casts/index expressions with collection literals #68456
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
Refine parsing of potentially ambiguous casts/index expressions with collection literals #68456
Conversation
…collection literals
| [Fact] | ||
| public void CastVersusIndexAmbiguity1() | ||
| { | ||
| // As a non-generic expression, we assume that `(type)` is an expression, and we are indexing into 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.
this preserves compat with existing code.
| [Fact] | ||
| public void CastVersusIndexAmbiguity12() | ||
| { | ||
| UsingExpression("(int[]?)[1, 2, 3]"); |
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.
adding a bunch more interesting variants to ensure we know the behavior and we don't accidentally regress here.
| public void CastVersusIndexAmbiguity14() | ||
| { | ||
| // Parenthesizing RHS should make this a cast. | ||
| UsingExpression("(type)([1, 2, 3])"); |
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.
explicit test to ensure that the user can take this route if they do happen to have a collection type that is non-generic (the likely case where thsi woudl arise would be aliases).
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 may be worth bringing up to LDM to discuss this. we coudl consider parenthesizing a simple identifier and indexing into it something we shoudl parse as a cast of a collection literal. it seems unlikely that people would need to parenthesize the identifier in that case.
| [Fact] | ||
| public void CastVersusIndexAmbiguity15() | ||
| { | ||
| UsingExpression("(alias::type)[1, 2, 3]"); |
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 is always a cast, as x::y can only refer to a type.
| [Fact] | ||
| public void CastVersusIndexAmbiguity20() | ||
| { | ||
| UsingExpression("(alias::type.member)[1, 2, 3]"); |
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.
unlike x::y, x::y.z can now be referring to a member.
| // PROTOTYPE: We allow `(X)[...]` as a cast of a collection expression. This could break existing | ||
| // code like (A.B.C)[0]. | ||
| // case SyntaxKind.OpenBracketToken: | ||
| case SyntaxKind.OpenBracketToken: |
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.
restored this to the original logic before i changed it in the feature branch. always thinking of [ as starting a literal after a (...) is likely too heavy a hammer. But it might be good to bring to LDM.
instead, we now do a more refined check above where we only think of this as a cast if we're sure what is in the (...) is a type. As nearly all collection types will be arrays/generic, this will just work.
| SyntaxKind.DotDotToken => true, | ||
| var tk => CanFollowCast(tk) | ||
| SyntaxKind.DotDotToken or | ||
| SyntaxKind.OpenBracketToken // for `(int[])[1, 2, 3]` or `(List<int>?)[1, 2, 3]` |
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.
Does this change affect any tests?
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.
it preserves existing tests passing.
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 haven't found any parsing tests that depend on or SyntaxKind.OpenBracketToken. Please add a parsing test that depends on this if there is not one already.
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.
CastVersusIndexAmbiguity8 validates this.
| UsingExpression("(F) [A] () => { }", TestOptions.RegularPreview, | ||
| // (1,1): error CS1073: Unexpected token '=>' | ||
| // (F) [A] () => { } | ||
| Diagnostic(ErrorCode.ERR_UnexpectedToken, "(F) [A] ()").WithArguments("=>").WithLocation(1, 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.
this restores the parsing behavior (in an error recovery case) here to the prior behavior we had here that i changed in the original parsing PR: https://github.com/dotnet/roslyn/pull/66417/files#diff-e77ec53b27dcea093de7ebc648dd17ff814931a0e44e001d5b6ab5d0a3403f0bR1255
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
src/Compilers/CSharp/Test/Syntax/Parsing/CollectionLiteralParsingTests.cs
Show resolved
Hide resolved
|
@RikkiGibson ptal. |
| case ScanTypeFlags.TupleType: | ||
| // If we have `(X<Y>)[...` then we know this must be a cast of a collection literal, not an index | ||
| // into some expr. As most collections are generic this means a good experience most of the time in | ||
| // this ambiguous scenario. |
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 sentence seems to indicate the generic case is ambiguous, even though the previous sentence implies it is not. Would it make sense to reword this second sentence? Perhaps: "As most collections are generic, the common case is not ambiguous."
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.
| public void CastVersusIndexAmbiguity19() | ||
| { | ||
| // something that starts looking generic, but isn't. | ||
| UsingExpression("(a < b > c)[1, 2, 3]"); |
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 bitter pill to me is the arbitrary nature of it. I definitely think this is a situation where we should be looking at making a breaking change. |
|
@RikkiGibson would you like to drive this with LDM? |
Yes |
|
I think auto-merge should be disabled until second review is complete. |
…nto castVsIndexParsing
|
@RikkiGibson Ptal :) |
|
@dotnet/roslyn-compiler @333fred for another pair of eyes plz. |
| UsingExpression("(A)[]", | ||
| // (1,5): error CS0443: Syntax error; value expected | ||
| // (A)[] | ||
| Diagnostic(ErrorCode.ERR_ValueExpected, "]").WithLocation(1, 5)); |
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.
Please do leave a prototype comment here about the diagnostic experience. If LDM approves this design the diagnostics in error cases (either "element access lacks a value" or "lookup didn't find an indexer" errors) should hint that user might want to parenthesize the [] if they meant to use a collection literal.
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.
will add in followup :)
This changes parsing of
(X)[1]back to the original C#1 interpretation of a parenthesized expression that is indexed into.Importantly though, the parsing of expressions like
(List<X>)[1, 2, 3]continues to parsed as a cast. This is because in the latter case, we know that the item in the parentheses must be a type, and so this could only be a cast. As collection literals are most commonly generic (or arrays) this means the common casts that people will perform will continue to be parsed as casts.One area this would have an impact on is:
The workaround on the user side is to write:
If we feel like this is a bitter pill (especially if lots of users alias complex collection instantiations) we could consider a breaking change in the language here to consider
(X)[...]to be a cast now. This feels like this could be fairly safe to to as it seems highly unlikely in practice that someone would parenthesize a simple identifier only to index into it. In other words, we would expect people to be writingX[...]not(X)[...]in the real world.