-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Record-structs: remove RecordStructDeclarationSyntax #52702
Conversation
I'm curious what you thought about the change. To me it reads better. What do you think? #Resolved |
@CyrusNajmabadi Got scared by last minute change and editing publicAPI is still annoying as hell at the moment, but I like the change. Thanks for the suggestion! #Resolved |
break; | ||
} | ||
break; | ||
|
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. this change alone makes me want this PR :) #Resolved
@@ -671,7 +671,7 @@ internal override bool TryGetBaseList(ExpressionSyntax expression, out TypeKindO | |||
{ | |||
if (node is BaseListSyntax) | |||
{ | |||
if (node.Parent is InterfaceDeclarationSyntax or StructDeclarationSyntax or RecordStructDeclarationSyntax) | |||
if (node.Parent is InterfaceDeclarationSyntax or StructDeclarationSyntax) |
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 feels like we need to add node.Parent.Kind() == SyntaxKind.RecordStruct
#Resolved
? SyntaxFactory.RecordDeclaration(SyntaxFactory.Token(SyntaxKind.RecordKeyword), namedType.Name.ToIdentifierToken()) | ||
: SyntaxFactory.RecordStructDeclaration(SyntaxFactory.Token(SyntaxKind.RecordKeyword), namedType.Name.ToIdentifierToken()); | ||
var declarationKind = namedType.TypeKind is TypeKind.Class ? SyntaxKind.RecordDeclaration : SyntaxKind.RecordStructDeclaration; | ||
typeDeclaration = SyntaxFactory.RecordDeclaration(declarationKind, SyntaxFactory.Token(SyntaxKind.RecordKeyword), namedType.Name.ToIdentifierToken()); |
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 RecordStructDeclaration cause SyntaxFactory.RecordDeclaration to create the struct
token? If not, we need to do that 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.
Definitely like the change. A couple of subtle IDE issues potentially.
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 should make supporting Edit and Continue just a couple of new cases in a couple of SyntaxKind switches 😄
: SyntaxFactory.RecordStructDeclaration(SyntaxFactory.Token(SyntaxKind.RecordKeyword), namedType.Name.ToIdentifierToken()); | ||
var isRecordClass = namedType.TypeKind is TypeKind.Class; | ||
var declarationKind = isRecordClass ? SyntaxKind.RecordDeclaration : SyntaxKind.RecordStructDeclaration; | ||
var classOrStructKeyword = SyntaxFactory.Token(isRecordClass ? default : SyntaxKind.ClassKeyword); |
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 on my phone, so might have missed something, but shouldn't this be SyntaxKind.StructKeyword
? Or does specifying the syntax kind mean it will automatically use a strict keyword? But if so, why specify the class keyword? #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.
I'm on my phone, so might have missed something, but shouldn't this be
SyntaxKind.StructKeyword
? Or does specifying the syntax kind mean it will automatically use a strict keyword? But if so, why specify the class keyword?
I agree, it looks like it should have been StructKeyword. Also needs tests if possible. #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.
Thanks. We don't yet have any scenarios that need generation of record structs, so this will get coverage when we do. #Resolved
Debug.Assert(builder.RecordDeclarationWithParameters is RecordDeclarationSyntax { ParameterList: not null } record | ||
&& record.Kind() == SyntaxKind.RecordStructDeclaration); |
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 made me realize a void Deconstruct(this SyntaxNode, out SyntaxKind)
might be actually useful. Currently matching multi-kind nodes is kind of ugly..
is RecordDeclarationSyntax(SyntaxKind.RecordStructDeclaration) { ParameterList: not null }
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.
That's a really interesting suggestion. I've also struggled with the fact that Kind is a method and can't be used with pattern matching.
@@ -671,7 +671,8 @@ internal override bool TryGetBaseList(ExpressionSyntax expression, out TypeKindO | |||
{ | |||
if (node is BaseListSyntax) | |||
{ | |||
if (node.Parent is InterfaceDeclarationSyntax or StructDeclarationSyntax or RecordStructDeclarationSyntax) | |||
if (node.Parent is InterfaceDeclarationSyntax or StructDeclarationSyntax | |||
|| node.Parent.Kind() == SyntaxKind.RecordStructDeclaration) |
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.
|| node.Parent.Kind() == SyntaxKind.RecordStructDeclaration) | |
|| node.Parent.IsKind(SyntaxKind.RecordStructDeclaration)) |
...ted/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Main.Generated.cs
Show resolved
Hide resolved
@@ -1142,8 +1139,7 @@ private Binder GetParameterNameAttributeValueBinder(MemberDeclarationSyntax memb | |||
return new WithParametersBinder(method.Parameters, nextBinder); | |||
} | |||
|
|||
if (memberSyntax is RecordDeclarationSyntax { ParameterList: { ParameterCount: > 0 } } | |||
or RecordStructDeclarationSyntax { ParameterList: { ParameterCount: > 0 } }) | |||
if (memberSyntax is RecordDeclarationSyntax { ParameterList: { ParameterCount: > 0 } }) |
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.
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. I have a list. The places that previously only handled RecordDeclarationSyntax are guarded (kind check or base type check). Some are guarded locally and are checked elsewhere (unreachable).
For the latter cases, I didn't see anything wrong with them. I could add assertions if you think that's useful.
An example is ExpressionVariableFinder.VisitRecordDeclaration
.
In reply to: 616728346 [](ancestors = 616728346)
case RecordStructDeclarationSyntax recordStructDeclaration when TryGetSynthesizedRecordConstructor(recordStructDeclaration) is SynthesizedRecordConstructor ctor: | ||
switch (declaredSymbol.Kind) | ||
{ | ||
case SymbolKind.Method: |
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.
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 seeing why (here or below). We get the same behavior. Do you mean just as an optimization?
In reply to: 616734766 [](ancestors = 616734766)
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.
Oh, I see. This would be necessary if we add assertion in PrimaryConstructorBaseType
as you suggested... Let me take a look
In reply to: 616930880 [](ancestors = 616930880,616734766)
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 seeing why (here or below). We get the same behavior. Do you mean just as an optimization?
I do not think we should look at bases for record structs even when they are present in an error scenario #Resolved
return true; | ||
}; | ||
|
||
case SymbolKind.NamedType: |
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.
@@ -2491,7 +2491,7 @@ private sealed class DeclaredMembersAndInitializersBuilder | |||
get { return _recordDeclarationWithParameters; } | |||
set | |||
{ | |||
Debug.Assert(value is RecordDeclarationSyntax or RecordStructDeclarationSyntax); | |||
Debug.Assert(value is RecordDeclarationSyntax); |
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.
}; | ||
ParameterListSyntax? paramList = declaredMembersAndInitializers.RecordDeclarationWithParameters is RecordDeclarationSyntax recordDecl | ||
? recordDecl.ParameterList | ||
: null; |
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.
declaredMembersAndInitializers.RecordDeclarationWithParameters?.ParameterList
? #Closed
_ => throw ExceptionUtilities.Unreachable | ||
}; | ||
var record = (RecordDeclarationSyntax)GetSyntax(); | ||
Debug.Assert(record.Kind() == SyntaxKind.RecordDeclaration || record.PrimaryConstructorBaseType is null); |
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.
@@ -671,7 +671,8 @@ internal override bool TryGetBaseList(ExpressionSyntax expression, out TypeKindO | |||
{ | |||
if (node is BaseListSyntax) | |||
{ | |||
if (node.Parent is InterfaceDeclarationSyntax or StructDeclarationSyntax or RecordStructDeclarationSyntax) | |||
if (node.Parent is InterfaceDeclarationSyntax or StructDeclarationSyntax | |||
|| node.Parent.Kind() == SyntaxKind.RecordStructDeclaration) |
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.
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. Or just use pattern matching against the Kind() #Resolved
{ | ||
get | ||
{ | ||
if (ClassOrStructKeyword.Kind() is SyntaxKind.StructKeyword) |
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.
@@ -271,7 +271,7 @@ internal override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSemanticMode | |||
{ | |||
if (MemberSymbol is SynthesizedRecordConstructor primaryCtor && | |||
primaryCtor.GetSyntax() is RecordDeclarationSyntax recordDecl && | |||
Root.FindToken(position).Parent?.AncestorsAndSelf().OfType<PrimaryConstructorBaseTypeSyntax>().FirstOrDefault() == recordDecl.PrimaryConstructorBaseType) | |||
Root.FindToken(position).Parent?.AncestorsAndSelf().OfType<PrimaryConstructorBaseTypeSyntax>().FirstOrDefault() == recordDecl.PrimaryConstructorBaseTypeIfClass) |
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.
Root.FindToken(position).Parent?.AncestorsAndSelf().OfType().FirstOrDefault() == recordDecl.PrimaryConstructorBaseTypeIfClass
I am assuming this method is never called for a record struct, otherwise it should never return true, even in error scenarios. It might be worth adding an assert at the top of the method that MemberSymbol
is not a primary constructor of a record struct. Also, I think we need to add a test that tries to get the speculative semantic model for a record struct at position of the first base when it has and doesn't have arguments. BTW, it looks like the is a bug, the following code compiles without any error:
public interface I1
{
void M01();
}
record struct R(int X) : I1(X)
{
void I1.M01() {}
}
``` #Closed
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.
There is also a bug for a regular record:
record R(int X) : I1()
{
void I1.M01() {}
}
No error. #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.
Added test BaseArguments_Speculation
. That test will reach the body of this conditional when using record
instead of record struct
. With record struct
we don't even get to the TryGetSpeculativeSemanticModelCore
method.
I think it's because of the logic in SyntaxTreeSemanticModel.GetMemberModel
which handles SyntaxKind.RecordDeclaration
but not SyntaxKind.RecordStructDeclaration
In reply to: 617049655 [](ancestors = 617049655)
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.
That matches my expectations, that is why I am suggesting to add an assert to ensure the method (not the body of the if) is not reachable for a record struct
Done with review pass (commit 6) In reply to: 823618084 |
void checkPrimaryConstructorBaseType(BaseTypeSyntax baseTypeSyntax, TypeSymbol baseType) | ||
{ | ||
if (baseTypeSyntax is PrimaryConstructorBaseTypeSyntax primaryConstructorBaseType && | ||
(!IsRecord || TypeKind == TypeKind.Struct || baseType.TypeKind == TypeKind.Interface || ((RecordDeclarationSyntax)decl.SyntaxReference.GetSyntax()).ParameterList is null)) |
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.
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 the first part: != Class
and == Struct
are equivalent since we're dealing with a record (IsRecord
check earlier on the line).
For the second part, the reason I checked for interface is because the other scenarios all already have errors (see below). #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.
For the first part: != Class and == Struct are equivalent since we're dealing with a record (IsRecord check earlier on the line).
For the second part, the reason I checked for interface is because the other scenarios all already have errors (see below).
It always more robust to not depend on other conditions and checking for what is required for the "good" state rather than trying to cover other possible "bad" states. The arguments are allowed for classes on base classes. I believe that the condition should be coded in these terms, rather than trying to cover what else might be there based on some previos conditions. #Resolved
@@ -500,6 +501,8 @@ private static BaseListSyntax GetBaseListOpt(SingleTypeDeclaration decl) | |||
baseType = baseBinder.BindType(typeSyntax, diagnostics, newBasesBeingResolved).Type; | |||
} | |||
|
|||
checkPrimaryConstructorBaseType(baseTypeSyntax, baseType); |
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.
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 parser only produces a PrimaryConstructorBaseType for the first element, so this check bails out on subsequent items already. I'm not sure what you're proposing. #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.
Added a check, but seems redundant to me...
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 check, but seems redundant to me...
I don't think it is redundant because we are not always dealing with trees from the parser. #Resolved
@@ -2224,11 +2224,7 @@ class C(int X, int Y) | |||
|
|||
if (!withParameters && withBaseArguments) |
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 with review pass (commit 8) |
record struct R1(int X) : Error(0, 1) | ||
{ | ||
} | ||
record struct R2(int X) : Error() |
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 should also try to speculate on that case without the argument list. #Closed
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.
Perhaps on position in the middle of the base type name
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.
LGTM (commit 13)
Test plan: #51199