-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Parse field
as a contextual keyword
#73947
Parse field
as a contextual keyword
#73947
Conversation
/// <summary> | ||
/// If an accessor kind changes, "field" and "value" within the accessor may need to be reinterpreted. | ||
/// </summary> | ||
internal bool IsInValueKeywordContext; |
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.
you might want to mention how this differs from the above. #Resolved
// Check if we have an initializer | ||
else if (this.CurrentToken.Kind == SyntaxKind.EqualsToken) | ||
// PROTOTYPE: SyntaxKind.GetAccessorDeclaration does seem correct for an initializer. | ||
// PROTOTYPE: Confirm that field should be a keyword, and bind to the backing field, in a property initializer: object P { get; } = field; |
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 interesting case for sure. i actually have no idea what should happen here. Property intitializers can't actually reference this
or fields. But if we do not have this bind to the field
keyword, this could then bind to somethgin higher... and i have no idea if we want that :) #Resolved
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.
Removed this case, assuming we will decide field
is not a keyword (and should not bind to the backing field) in the property initializer.
private readonly struct FieldAndValueKeywordContext : IDisposable | ||
{ | ||
private readonly LanguageParser _parser; | ||
private readonly (bool, bool) _isInFieldAndValueKeywordContext; |
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.
maybe _originalXXX
to indicate it was the starting value. #Resolved
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.
nit: separate fields just seems simpler :)
@@ -4191,6 +4221,9 @@ private AccessorDeclarationSyntax ParseAccessorDeclaration(bool isEvent) | |||
accessorName = ConvertToKeyword(accessorName); | |||
} | |||
|
|||
// PROTOTYPE: How should ‘field’ and ‘value’ be interpreted in the attributes, modifiers? Those parts have already been parsed. |
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.
we already decided this in ldm, but the answer, afair, is: they are not in scope there. #Resolved
@@ -4266,6 +4299,20 @@ private static SyntaxKind GetAccessorKind(SyntaxToken accessorName) | |||
}; | |||
} | |||
|
|||
private static (bool, bool) GetFieldAndValueKeywordContext(AccessorDeclaringKind declaringKind, SyntaxKind accessorKind) |
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.
name these bools. #Resolved
{ | ||
return accessorKind switch | ||
{ | ||
SyntaxKind.GetAccessorDeclaration => (declaringKind == AccessorDeclaringKind.Property, 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.
named args for the literals. #Resolved
@@ -5108,7 +5155,7 @@ private static bool CanReuseVariableDeclarator(CSharp.Syntax.VariableDeclaratorS | |||
// declaration. However, in the specific case of variable declarators, Dev10 | |||
// specifically treats it as a variable name, even if it could be interpreted as a | |||
// keyword. | |||
var name = this.ParseIdentifierToken(); | |||
var name = this.ParseIdentifierToken(reportErrorRatherThanConvertingFieldOrValue: 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.
for somethign as core as ParseIdentifierToken, it feels very strange to need such a specialized arg. esp. since we haven't needed that sort of arg for other contextual tokens? #Resolved
@@ -5439,7 +5486,7 @@ private EnumMemberDeclarationSyntax ParseEnumMemberDeclaration() | |||
equalsValue = _syntaxFactory.EqualsValueClause( | |||
this.EatToken(SyntaxKind.EqualsToken), | |||
this.CurrentToken.Kind is SyntaxKind.CommaToken or SyntaxKind.CloseBraceToken | |||
? this.ParseIdentifierName(ErrorCode.ERR_ConstantExpected) | |||
? this.ParseIdentifierName(null, ErrorCode.ERR_ConstantExpected) |
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.
named arg for literal. #Resolved
SyntaxToken identifierToken = this.EatToken(); | ||
|
||
if (this.IsInAsync && identifierToken.ContextualKind == SyntaxKind.AwaitKeyword) | ||
{ | ||
identifierToken = this.AddError(identifierToken, ErrorCode.ERR_BadAwaitAsIdentifier); | ||
} | ||
else if (isFieldOrValueInKeywordContext) // PROTOTYPE: Are we testing this with "value"? | ||
{ | ||
if (reportErrorRatherThanConvertingFieldOrValue.GetValueOrDefault()) |
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.
not gettnig why we need this bool?
. seems out of sorts with how we genrally do contexts and parsing of identifiers/contextual-keywords. #Resolved
@@ -8562,7 +8641,7 @@ private bool IsPossibleStatement(bool acceptAccessibilityMods) | |||
return true; | |||
|
|||
case SyntaxKind.IdentifierToken: | |||
return IsTrueIdentifier(); | |||
return IsTrueIdentifier() || IsCurrentTokenFieldOrValueInKeywordContext(); |
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 logic is what IsTrueIdentifier is supposed to already check. #Resolved
@@ -11624,11 +11718,11 @@ private ArgumentSyntax ParseArgumentExpression(bool isIndexer) | |||
|
|||
if (isIndexer && this.CurrentToken.Kind is SyntaxKind.CommaToken or SyntaxKind.CloseBracketToken) | |||
{ | |||
expression = this.ParseIdentifierName(ErrorCode.ERR_ValueExpected); | |||
expression = this.ParseIdentifierName(null, ErrorCode.ERR_ValueExpected); |
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.
named parametres. #Resolved
@@ -13722,17 +13834,26 @@ private new struct ResetPoint | |||
internal readonly TerminatorState TerminatorState; | |||
internal readonly bool IsInAsync; | |||
internal readonly bool IsInQuery; | |||
// PROTOTYPE: Why do we need this? We don't need something similar for 'partial' |
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.
we need this because partial can look locally to figure out what it means. it is not affected by outer contexts. that's not the case for async/queries/field/value. field/value shuld be treated very closely to async/await. An outer context pushes us into a context where those tokens now become keywords.
fortunately field/value is also simpler because we never change the value as we go deeper into the accessor.
</TypeComment> | ||
<FactoryComment> | ||
<summary>Creates a ValueExpressionSyntax node.</summary> | ||
</FactoryComment> |
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.
Alternatively, we could use LiteralExpressionSyntax, just like we use that for boolean expressions with true
and false
keyword. but i'm not sure how compiler would feel about fallout of these being thought of as literals, so i think it's totally ok to not do that. #Resolved
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 leave as is.
@@ -286,6 +286,9 @@ internal enum NodeFlags : ushort | |||
ContainsStructuredTrivia = 1 << 9, | |||
|
|||
InheritMask = IsNotMissing | ContainsAnnotations | ContainsAttributes | ContainsDiagnostics | ContainsDirectives | ContainsSkippedText | ContainsStructuredTrivia, | |||
|
|||
FactoryContextIsInFieldKeywordContext = 1 << 10, | |||
FactoryContextIsInValueKeywordContext = 1 << 11, |
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 would move these up next to the other FactoryContext values. #Resolved
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.
note: this also makes us out of bits again.
} | ||
} | ||
|
||
internal bool ParsedInValueKeywordContext |
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.
where is the code for this checked. there should be some point that is checking this, and thus ensuring that if we do an incremental parse, where a statement used to be in a property, and it moves to be in a method, that the meaning of field/value
go back to being an identifier (and vice versa).
note: this is not a strange scenario to be in. people convert properties to methods and back. So incremental parsing def needs to be good 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.
Is this property necessary? To switch the context between a property accessor and a method would require changing the containing member definition kind which I suspect invalidates the body of property, accessor, or method.
done with initial pass. |
393e476
to
538db7e
Compare
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've looked through most of the tests, but not FieldKeywordTests.cs
yet. Will get to that later.
src/Compilers/CSharp/Test/Syntax/Parsing/FieldKeywordParsingTests.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Syntax/Parsing/FieldKeywordParsingTests.cs
Outdated
Show resolved
Hide resolved
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.
Looks good but haven't gotten through tests yet.
{ | ||
string name = identifier.Text; | ||
var requiredVersion = MessageID.IDS_FeatureFieldKeyword.RequiredVersion(); | ||
diagnostics.Add(ErrorCode.INF_IdentifierConflictWithContextualKeyword, syntax, name, requiredVersion.ToDisplayString()); |
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.
Is this an open issue that we are tracking resolving in a future PR?
{ | ||
accessorsHaveImplementation = true; | ||
} | ||
|
||
usesFieldKeyword = usesFieldKeyword || containsFieldKeyword(accessor); |
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 other usage of this local function passes the body
as an argument, should we do the same here? #Resolved
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 reason for passing accessor
rather than body
is to include attributes from the accessor.
isReadOnly: (hasGetAccessor && !hasSetAccessor) || isInitOnly, | ||
// Synthesized backing field for 'field' should not be marked 'initonly' | ||
// since the field might be modified in the get accessor. | ||
isReadOnly: !usesFieldKeyword && ((hasGetAccessor && !hasSetAccessor) || isInitOnly), |
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'm not sure I follow this logic.. what if we are in a readonly
property or a property on a readonly struct? But it seems like this would give a non-readonly
field in that case.
I would expect to be able to use the field
keyword in such cases, but it would need to refer to a readonly
field. You would be able to write to the field
in an init
accessor, or a property initializer, or by assigning to the property in the constructor.
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.
Should the synthesized field of a readonly property be readonly, or should the field be mutable, regardless? Should this be an open question for language design?
class C
{
object P
{
get
{
field ??= ""; // ok?
return field;
}
}
}
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 field should be writable except when the accessor is readonly
in the "readonly modifier" sense. i.e. the get-only case you have shown here should be writable.
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.
Added a PROTOTYPE comment for now.
@@ -230,21 +230,11 @@ protected void SetFactoryContext(SyntaxFactoryContext context) | |||
{ | |||
SetFlags(NodeFlags.FactoryContextIsInQuery); | |||
} | |||
} | |||
|
|||
internal static NodeFlags SetFactoryContext(NodeFlags flags, SyntaxFactoryContext context) |
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.
Was this just dead code? #Resolved
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.
Yes.
|
||
static bool containsFieldKeyword(SyntaxNode syntax) | ||
{ | ||
return syntax.DescendantTokens().Any(static t => t.Kind() == SyntaxKind.FieldKeyword); |
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 traversal avoid creating the red tree? #Resolved
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.
No, but haven't we already created the red tree for the property simply by referencing the root SyntaxNode
?
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.
no. walking down the red tree causes the red allocations. ideally this walk is done on the green tree to avoid actual allocs.
} | ||
} | ||
"""; | ||
var verifier = CompileAndVerify(source, expectedOutput: """ |
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 will be good to also verify contents of compilation.GetMember("C").GetMembers()
. I would expect that because other backing fields are present there that this would be too.
No need to do this across all the tests, just one or two places would probably be fine. #Resolved
comp.VerifyEmitDiagnostics( | ||
// (5,22): info CS9258: 'field' is a contextual keyword in property accessors starting in language version preview. Use '@field' instead. | ||
// get { return field; } | ||
Diagnostic(ErrorCode.INF_IdentifierConflictWithContextualKeyword, "field").WithArguments("field", "preview").WithLocation(5, 22), |
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 am confused by these INF diagnostics. If we are already in LangVersion preview I wouldn't expect them to be reported. These will show up in the IDE error list if someone is just using the feature normally and intentionally. Is this something we're planning on fixing later?
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 believe we want to report this info-level diagnostic always, even with language version preview, to allow users to discover where there may be a breaking change. But perhaps we should only report the diagnostic when field
would otherwise bind to a member other than the synthesized field. cc @jaredpar
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 think reporting it in all LangVersions made sense when we were just shipping the diagnostic in order to grasp the severity of the potential break. I don't think it makes sense to report in the case that the feature is being used normally and there is no symbol field
in scope.
I have a lot more thoughts here, but basically, I think there is a need to design this experience more deeply/holistically, thinking about what should happen in various combinations of LangVersion, and the kind of declaration which is named field
, and the way the field
identifier/keyword is being used.
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.
Added a PROTOTYPE comment to Binder.ReportFieldContextualKeywordConflict
.
} | ||
|
||
[Fact] | ||
public void FieldInInitializer_03() |
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.
tests for case when accessor uses field
and property has an initializer? #Resolved
} | ||
|
||
[Fact] | ||
public void Attribute_02() |
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.
also tests for when property or accessor attribute uses [field: ...]
? I would expect these to behave the same as they do on auto-properties today.
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've added an item to the test plan to verify attributes can be attached to the synthesized field using [field: ...]
, and I'll handle that in a subsequent PR.
N(SyntaxKind.EqualsValueClause); | ||
{ | ||
N(SyntaxKind.EqualsToken); | ||
IdentifierNameOrFieldExpression(languageVersion, escapeIdentifier: false, expectedParsedAsToken); |
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 feels like it would be more straightforward to just use the generated baseline when we expect it to be the same in all the cases of the theory. #Resolved
@@ -561,6 +582,7 @@ class C | |||
[CombinatorialValues(LanguageVersion.CSharp12, LanguageVersion.Preview)] LanguageVersion languageVersion) | |||
{ | |||
string source = """ | |||
#pragma warning disable 168 |
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 include something like the message of the diagnostic which is being disabled or its name in ErrorCode to hint at what is going on here. #Resolved
@@ -3876,7 +3879,32 @@ private SyntaxToken TryEatCheckedOrHandleUnchecked(ref SyntaxToken operatorKeywo | |||
semicolon); | |||
} | |||
|
|||
private AccessorListSyntax ParseAccessorList(bool isEvent) | |||
private readonly struct FieldKeywordContext : IDisposable |
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.
could make a ref-struct, so this doesn't get put on the heap accidentally. #Resolved
Spec change: dotnet/csharplang#8239, dotnet/csharplang#8283