-
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
Add support for init-only members #43275
Conversation
0475dde
to
c69d069
Compare
Will update this PR based on today's LDM decisions. #Closed |
e9aea11
to
5cc692a
Compare
230ad67
to
f83c7a5
Compare
@dotnet/roslyn-compiler This PR is open for review. I've updated this PR with latest decisions from LDM and API design meeting:
|
@@ -717,24 +717,32 @@ private TypeSymbol GetTypeOfTypeDef(TypeDefinitionHandle typeDef, out bool isNoP | |||
{ | |||
var isAllowed = false; | |||
|
|||
switch (allowedRequiredModifierType) | |||
if ((allowedRequiredModifierType & AllowedRequiredModifierType.System_Runtime_InteropServices_InAttribute) != 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.
if ((allowedRequiredModifierType & AllowedRequiredModifierType.System_Runtime_InteropServices_InAttribute) != 0 && [](start = 20, length = 114)
It looks like these changes are going to conflict with changes made in #43355. Consider coordinating with @333fred to make changes in both PRs in a manner that would minimize the churn during merge #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.
Fred, this change allows for looking for more than one modreq at a certain position. Would you mind taking this change into your branch to minimize merge conflicts?
In reply to: 410990682 [](ancestors = 410990682)
@@ -1178,17 +1186,18 @@ private void DecodeParameterOrThrow(ref BlobReader signatureReader, /*out*/ ref | |||
{ | |||
info.CustomModifiers = DecodeModifiersOrThrow( | |||
ref signatureReader, | |||
AllowedRequiredModifierType.System_Runtime_InteropServices_InAttribute, | |||
AllowedRequiredModifierType.System_Runtime_InteropServices_InAttribute | | |||
AllowedRequiredModifierType.System_Runtime_CompilerServices_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.
AllowedRequiredModifierType.System_Runtime_CompilerServices_IsInitOnly [](start = 20, length = 70)
Allowing this on any parameter doesn't feel right. #Closed
@@ -1592,6 +1595,9 @@ If such a class is used as a base class and if the deriving class defines a dest | |||
<data name="ERR_ExplicitPropertyAddingAccessor" xml:space="preserve"> | |||
<value>'{0}' adds an accessor not found in interface member '{1}'</value> | |||
</data> | |||
<data name="ERR_ExplicitPropertyMismatchInitOnly" xml:space="preserve"> | |||
<value>Accessors '{0}' and '{1}' don't match by init-only</value> |
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.
don't match by init-only [](start = 37, length = 24)
Consider rewording for clarity. #Closed
@@ -2891,7 +2897,7 @@ A catch() block after a catch (System.Exception e) block can catch non-CLS excep | |||
<value>Members of readonly field '{0}' cannot be used as a ref or out value (except in a constructor)</value> | |||
</data> | |||
<data name="ERR_AssgReadonly" xml:space="preserve"> | |||
<value>A readonly field cannot be assigned to (except in a constructor of the class in which the field is defined or a variable initializer))</value> | |||
<value>A readonly field cannot be assigned to (except in a constructor or init-only member of the class in which the field is defined or a variable initializer))</value> |
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.
init-only member [](start = 78, length = 16)
Is there a concept of an "init-only member"? #Closed
@@ -6067,4 +6079,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ | |||
<data name="WRN_GeneratorFailedDuringInitialization_Title" xml:space="preserve"> | |||
<value>Generator failed to initialize.</value> | |||
</data> | |||
<data name="ERR_AssignmentInitOnly" xml:space="preserve"> | |||
<value>Init-only member '{0}' can only be assigned from a constructor, object initialization or 'with' expression of that type.</value> |
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.
from [](start = 55, length = 4)
"in" vs. "from"? #Closed
@@ -200,6 +200,7 @@ public enum SyntaxKind : ushort | |||
WhenKeyword = 8437, | |||
DataKeyword = 8438, | |||
WithKeyword = 8439, | |||
InitKeyword = 8440, | |||
/// when adding a contextual keyword following functions must be adapted: | |||
/// <see cref="SyntaxFacts.GetContextualKeywordKinds"/> | |||
/// <see cref="SyntaxFacts.IsContextualKeyword(SyntaxKind)"/> |
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.
/// [](start = 8, length = 61)
Consider also adding GetContextualKeywordKind to the list #Closed
I am suggesting to add a prototype comment, explicitly stating that we expect an error on the In reply to: 620859393 [](ancestors = 620859393,620753699) Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs:2862 in 7cce780. [](commit_id = 7cce780, deletion_comment = False) |
FWIW, the reason why it's a question in my mind is because the decoding of the accessors is done before we decide that they are accessors. In reply to: 620760072 [](ancestors = 620760072) Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs:3128 in 1d77553. [](commit_id = 1d77553, deletion_comment = False) |
I can understand that you have a question about how to do that, i.e. how to implement. However, there is no question what the behavior should be, i.e. what the expectations are. And the expectations are that we should be more restrictive. No question. In reply to: 620863035 [](ancestors = 620863035,620760072) Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs:3128 in 7cce780. [](commit_id = 7cce780, deletion_comment = False) |
And we also certainly can do that. Again, no question. In reply to: 620865345 [](ancestors = 620865345,620863035,620760072) Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs:3128 in 7cce780. [](commit_id = 7cce780, deletion_comment = False) |
|
||
if (typeCode == SignatureTypeCode.ByReference) | ||
{ | ||
info.IsByRef = true; | ||
|
||
// PROTOTYPE(init-only): we're incorrectly letting modreq(IsExternalInit) into the RefCustomModifiers |
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.
// PROTOTYPE(init-only): we're incorrectly letting modreq(IsExternalInit) into the RefCustomModifiers [](start = 16, length = 101)
It looks like this comment is obsolete, the if
at the end of this block takes care of the issue. BTW, consider moving that if
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.
I'll remove the comment. Thanks
It's a nit, but the reason the if/throw
is at the end of this block is that the decoded modreq will get stored rather than lost.
In reply to: 417491981 [](ancestors = 417491981)
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 the if/throw is at the end of this block is that the decoded modreq will get stored rather than lost.
This isn't really useful. I prefer us to follow consistent handling of unsupported modereqs within this type - throw as soon as we recognize the fact
In reply to: 417569537 [](ancestors = 417569537,417491981)
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 might make sense to do that after the assignmentinfo.RefCustomModifiers = info.CustomModifiers
, but it is really not worth trying to decode the next set of modifiers because we aren't even going to try decoding the type of the parameter.
In reply to: 417585593 [](ancestors = 417585593,417569537,417491981)
return false; | ||
} | ||
|
||
if ((!method.IsStatic && method.IsConstructor()) || method.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.
(!method.IsStatic && method.IsConstructor()) [](start = 20, length = 44)
Why so complicated? Check out how IsConstructor
is implemented. method.MethodKind == MethodKind.Constructor
? #Closed
@@ -160,13 +161,21 @@ public override void VisitProperty(IPropertySymbol symbol) | |||
AddPunctuation(SyntaxKind.OpenBraceToken); | |||
|
|||
AddAccessor(symbol, symbol.GetMethod, SyntaxKind.GetKeyword); | |||
AddAccessor(symbol, symbol.SetMethod, SyntaxKind.SetKeyword); | |||
// PROTOTYPE(init-only): adjust SymbolDisplayVisitor once we have a public IsInitOnly API |
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.
// PROTOTYPE(init-only): adjust SymbolDisplayVisitor once we have a public IsInitOnly API [](start = 16, length = 89)
Consider moving the comment into the IsInitOnly
helper below, this is where the change should happen. #Closed
@@ -190,7 +199,7 @@ private void AddPropertyNameAndParameters(IPropertySymbol symbol) | |||
this.builder.Add(CreatePart(SymbolDisplayPartKind.PropertyName, symbol, symbol.Name)); | |||
} | |||
|
|||
if (this.format.MemberOptions.IncludesOption(SymbolDisplayMemberOptions.IncludeParameters) && symbol.Parameters.Any()) | |||
if (this.format.MemberOptions.IncludesOption(SymbolDisplayMemberOptions.IncludeParameters) && symbol.Parameters.Any<IParameterSymbol>()) |
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.
[](start = 127, length = 18)
This looks like an unintentional change. #Closed
|
||
AddSpace(); | ||
AddPunctuation(SyntaxKind.CloseBraceToken); | ||
} | ||
} | ||
|
||
private static bool IsInitOnly(IPropertySymbol symbol) | ||
{ | ||
return (symbol.SetMethod as Symbols.PublicModel.MethodSymbol)?.UnderlyingMethodSymbol.IsInitOnly == 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.
symbol.SetMethod [](start = 20, length = 16)
Consider extracting a helper that takes IMethodSymbol, so that we can use it below. #Closed
@@ -410,7 +419,8 @@ public override void VisitMethod(IMethodSymbol symbol) | |||
} | |||
AddPropertyNameAndParameters(associatedProperty); | |||
AddPunctuation(SyntaxKind.DotToken); | |||
AddKeyword(symbol.MethodKind == MethodKind.PropertyGet ? SyntaxKind.GetKeyword : SyntaxKind.SetKeyword); | |||
AddKeyword(symbol.MethodKind == MethodKind.PropertyGet ? SyntaxKind.GetKeyword : | |||
IsInitOnly(associatedProperty) ? SyntaxKind.InitKeyword : SyntaxKind.SetKeyword); |
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.
IsInitOnly(associatedProperty) [](start = 28, length = 30)
It feels like we should have a helper that takes IMethodSymbol. We have a method at hand, why go back to the property? #Closed
TypeWithAnnotations returnType, | ||
ImmutableArray<CustomModifier> refCustomModifiers, | ||
ImmutableArray<MethodSymbol> explicitInterfaceImplementations) | ||
{ | ||
Debug.Assert(isInitOnly == CustomModifierUtils.HasIsExternalInitModifier(returnType.CustomModifiers)); |
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.
Debug.Assert(isInitOnly == CustomModifierUtils.HasIsExternalInitModifier(returnType.CustomModifiers)); [](start = 12, length = 102)
Not quite the assert I suggested to add, but, if it holds, let's keep it this way. #Closed
Done with review pass (iteration 15) #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.
LGTM (iteration 16)
|
||
internal static bool HasIsExternalInitModifier(this ImmutableArray<CustomModifier> modifiers) | ||
{ | ||
if (modifiers.IsDefault) |
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.
if (modifiers.IsDefault) [](start = 12, length = 24)
Why do we need this check here, but not in the helper above. What is different? #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.
This feels wrong, It feels unexpected for a symbol to expose modifiers as ImmutableArray<CustomModifier>.Default
. I think we should understand the root cause.
In reply to: 417705806 [](ancestors = 417705806)
@@ -265,7 +265,7 @@ internal TypeWithAnnotations MergeEquivalentTypes(TypeWithAnnotations other, Var | |||
/// <summary> | |||
/// The list of custom modifiers, if any, associated with the <see cref="Type"/>. | |||
/// </summary> | |||
public ImmutableArray<CustomModifier> CustomModifiers => _extensions.CustomModifiers; | |||
public ImmutableArray<CustomModifier> CustomModifiers => _extensions?.CustomModifiers ?? default; |
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.
_extensions?.CustomModifiers ?? default; [](start = 65, length = 40)
Has something changed? Why do we need to add this logic in this PR? #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.
CI flagged an issue (null ref crash)
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.
in this assertion in SignatureOnlyMethodSymbol:
Debug.Assert(isInitOnly == CustomModifierUtils.HasIsExternalInitModifier(returnType.CustomModifiers));
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.
@AlekseyTs Two more tests failed in CI, desktop only. I'm pushing fixes now. Thanks
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.
in this assertion in SignatureOnlyMethodSymbol:
Debug.Assert(isInitOnly == CustomModifierUtils.HasIsExternalInitModifier(returnType.CustomModifiers));
Why do we have a a type without _extensions? Is it default value of the structure? Why SignatureOnlyMethodSymbol uses it? I think we should understand the root cause, other places could be affected as well.
In reply to: 417715941 [](ancestors = 417715941)
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 hit this problem in PerformCrefOverloadResolution
which does pass a default
type.
The reason I'm fixing the issue in TypeWithAnnotations
is that it mostly already handled when _extensions
is null. Do you think it would be better to return an empty array instead?
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 hit this problem in PerformCrefOverloadResolution which does pass a default type.
It looks like the comparer that is used there is supposed to ignore return type and also is supposed to ignore custom modifiers. So. I think we need to understand why exactly do we get here for the default instance, what are we trying to do and why.
The reason I'm fixing the issue in TypeWithAnnotations is that it mostly already handled when _extensions is null. Do you think it would be better to return an empty array instead?
I am not sure if we even should touch this property. Default instances of this structure are not safe by design, I see other properties that will throw or return something that consumers would not expect normally. We simply shouldn't be operating on them.
In reply to: 418013638 [](ancestors = 418013638)
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.
If the new assert is what causing the problem, then I think we should simply adjust it to deal with default instance and not access the property.
In reply to: 418044532 [](ancestors = 418044532,418013638)
@jcouv Please do not merge without closing the loop for two active threads related to recent changes. #Closed |
A discussion is ongoing around more recent changes
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 (iteration 22)
Part of records feature:
Speclet: https://github.com/dotnet/csharplang/blob/master/proposals/init.md
The first commit only has a refactoring (splitting and flattening very long/nested method).
A couple of design points that I expect might be need discussion:
Note: the first commit is a refactoring to one file.