-
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
Disallow record with ambiguous constructors #49975
Conversation
public void AmbigCtor() | ||
{ | ||
var src = @" | ||
record R(R x); |
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 what I understand, this change is specific to positional records only.
Shouldn't the same error be issued for something like the following:
record R(string S)
{
public R(R x) {}
};
``` #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.
Shouldn't the same error be issued for something like the following:
Is there a problem with this scenario?
In reply to: 543321591 [](ancestors = 543321591)
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 there a problem with this scenario?
@AlekseyTs It's currently allowed in master.
The PR doesn't seem to disallow it.
So my question was: should this scenario (explicit declaration of a copy constructor) be disallowed? #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.
So my question was: should this scenario (explicit declaration of a copy constructor) be disallowed?
Does this violate specification? What would be the reason to do this?
In reply to: 543402442 [](ancestors = 543402442)
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 violate specification?
On its current state, I think no.
What would be the reason to do this?
It's still ambiguous (if I'm not missing something). Also, if it will replace the synthesized copy constructor, probably with
initializers won't work as expected in this case (because the <Clone>$
method depends on the copy constructor). So at minimum I'd expect either a compiler warning or an IDE suggestion (or a CA code quality rule) to warn for this. #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.
It's still ambiguous.
With what?
(if I'm not missing something)
This PR is about detecting an ambiguity between a synthesized copy constructor and a primary constructor. This ambiguity for explicitly declared copy constructor is already detected, I think.
So at minimum I'd expect either a compiler warning or an IDE suggestion (or a CA code quality rule) to warn for this.
Explicit declaration of a copy constructor is intentionally supported/allowed (https://github.com/dotnet/csharplang/blob/master/proposals/csharp-9.0/records.md#copy-and-clone-members).
In reply to: 543424682 [](ancestors = 543424682)
It looks like this PR is not related to the referenced issue. #Closed |
@@ -6591,4 +6591,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ | |||
<data name="WRN_UnreadRecordParameter_Title" xml:space="preserve"> | |||
<value>Parameter is unread. Did you forget to use it to initialize the property with that name?</value> | |||
</data> | |||
<data name="ERR_RecordAmbigCtor" xml:space="preserve"> | |||
<value>A record cannot be declared with a single positional parameter with the type of that record.</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.
with the type of that record [](start = 74, length = 28)
"of the same type as the record"? #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.
I think it would be better to report more specific error. Some thing like: "A copy constructor cannot be synthesized for a record with the only positional parameter of the same type as the record."
In reply to: 543383571 [](ancestors = 543383571)
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.
"Synthesized copy constructor conflicts with a primary constructor." is even better, I think.
In reply to: 543408078 [](ancestors = 543408078,543383571)
if (ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this)) | ||
{ | ||
diagnostics.Add(ErrorCode.ERR_RecordAmbigCtor, ctor.Locations[0]); | ||
return 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.
return null; [](start = 20, length = 12)
This doesn't feel right. I think we should suppress creation of synthesized copy constructor instead. The primary constructor is explicitly declared, we cannot just ignore the declaration even if it is erroneous. #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.
If we suppress the primary constructor, then we'll also get a warning for unused parameter. I think it doesn't really matter which one we suppress. In my mind the copy constructor is always there, and in this case prevents the primary constructor from being declared. I don't see a problem.
In reply to: 543389753 [](ancestors = 543389753)
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 we suppress the primary constructor, then we'll also get a warning for unused parameter
I am not following. It looks like you are suppressing it.
In reply to: 543463726 [](ancestors = 543463726,543389753)
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.
Whatever unexpected warning we get, should be suppressed, of course. But that should not come at the "expense" of symbols.
In my mind the copy constructor is always there, and in this case prevents the primary constructor from being declared. I don't see a problem.
I do. The primary constructor is clearly declared. So, there should be a corresponding symbol for it. I am also not suggesting to suppress copy constructor. All we need to do is to detect an ambiguity and report it.
In reply to: 543465253 [](ancestors = 543465253,543463726,543389753)
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 also not suggesting to suppress copy constructor.
To clarify. I did suggest this as an alternative at the beginning because that constructor is synthesized and I would be comfortable with not synthesizing something "illegal" as opposed to not creating a symbol for something explicitly declared in code. However, I would be fine with leaving that symbol along as well. We could get in a situation of both symbols present when copy constructor is declared explicitly, therefore, the pipeline should be able to operate reliably with both symbols present.
In reply to: 543470457 [](ancestors = 543470457,543465253,543463726,543389753)
members.Add(ctor); | ||
return ctor; | ||
|
||
if (ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this)) |
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.
Equals(this) [](start = 72, length = 12)
All aspects should be ignored here, I think. #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.
We're already ignoring differences that can appear in source code (nullability). But I think you're right in terms of future-proofing. Thanks
In reply to: 543392514 [](ancestors = 543392514)
|
||
if (ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this)) | ||
{ | ||
diagnostics.Add(ErrorCode.ERR_RecordAmbigCtor, ctor.Locations[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.
diagnostics.Add(ErrorCode.ERR_RecordAmbigCtor, ctor.Locations[0]); [](start = 20, length = 66)
I am not sure if we should perform the check and report this error for a case when a copy constructor is declared explicitly, we already report an error in that scenario. #Closed
record R(R x); | ||
|
||
#nullable enable | ||
record R2(R2? x) { } |
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.
What happens if this parameter is referenced in a field initializer. What information is going to be returned for that reference by SemanticModel? #Closed
@@ -229,6 +229,34 @@ class E | |||
comp.VerifyDiagnostics(); | |||
} | |||
|
|||
[Fact, WorkItem(49628, "https://github.com/dotnet/roslyn/issues/49628")] | |||
public void AmbigCtor() |
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.
AmbigCtor [](start = 20, length = 9)
We should also test a case when an ambiguous (against primary constructor) copy constructor is declared explicitly. #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.
Note, prior to this change we already reported an expected error for a scenario like that:
record R(R y)
{
public R(R x) {}
}
error CS0111: Type 'R' already defines a member called '.ctor' with the same parameter types
There is no need to change error reporting for a scenario like that.
In reply to: 543414763 [](ancestors = 543414763)
Done with review pass (commit 1) #Closed |
members.Add(ctor); | ||
return ctor; | ||
|
||
if (ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this)) |
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 (ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this)) [](start = 16, length = 69)
Do we understand why a regular process of detecting ambiguous members doesn't detect the ambiguity? #Closed
@@ -6592,6 +6592,6 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ | |||
<value>Parameter is unread. Did you forget to use it to initialize the property with that name?</value> | |||
</data> | |||
<data name="ERR_RecordAmbigCtor" xml:space="preserve"> | |||
<value>A record cannot be declared with a single positional parameter with the type of that record.</value> | |||
<value>Synthesized copy constructor conflicts with a primary constructor</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.
Should end with a period? #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.
@@ -397,7 +397,7 @@ internal static bool IncludeFieldInitializersInBody(this MethodSymbol methodSymb | |||
return methodSymbol.IsConstructor() | |||
&& !methodSymbol.HasThisConstructorInitializer() | |||
&& !(methodSymbol is SynthesizedRecordCopyCtor) // A record copy constructor is special, regular initializers are not supposed to be executed by it. | |||
&& !Binder.IsUserDefinedRecordCopyConstructor(methodSymbol); | |||
&& (!Binder.IsUserDefinedRecordCopyConstructor(methodSymbol) || methodSymbol is SynthesizedRecordConstructor); |
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.
IsUserDefinedRecordCopyConstructor [](start = 28, length = 34)
I think IsUserDefinedRecordCopyConstructor
should be adjusted to not confuse primary constructor with a copy constructor. #Closed
if (!(paramList is null)) | ||
{ | ||
Debug.Assert(builder.RecordDeclarationWithParameters is object); | ||
Debug.Assert(builder.InstanceInitializersForRecordDeclarationWithParameters is object); | ||
|
||
var ctor = addCtor(builder.RecordDeclarationWithParameters); | ||
|
||
if (ctor.ParameterCount != 0) | ||
if (ctor is not null && ctor.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.
ctor is not null [](start = 20, length = 16)
This condition is always true. #Closed
{ | ||
var existingOrAddedMembers = addProperties(ctor.Parameters); | ||
addDeconstruct(ctor, existingOrAddedMembers); | ||
} | ||
if (ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this, TypeCompareKind.AllIgnoreOptions)) |
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.
ctor.ParameterCount == 1 && ctor.Parameters[0].Type.Equals(this, TypeCompareKind.AllIgnoreOptions) [](start = 20, length = 98)
Consider simply assigning this value. #Closed
); | ||
|
||
var r = comp.GlobalNamespace.GetTypeMember("R"); | ||
Assert.Equal(new[] { "R..ctor(R x)", "R..ctor(R x)", "R..ctor()" }, r.GetMembers(".ctor").ToTestDisplayStrings()); |
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.
"R..ctor()" [](start = 65, length = 11)
A parameter-less constructor shouldn't be synthesized for this scenario. We should review all consumers of HasCopyConstructorSignature
and make sure they properly deal with the ambiguity. #Closed
); | ||
|
||
var r = comp.GlobalNamespace.GetTypeMember("R"); | ||
Assert.Equal(new[] { "R..ctor(R x)", "R..ctor(R original)", "R..ctor()" }, r.GetMembers(".ctor").ToTestDisplayStrings()); |
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.
"R..ctor()" [](start = 72, length = 11)
A parameter-less constructor shouldn't be synthesized. #Closed
Done with review pass (commit 3) #Closed |
// (8,33): error CS0121: The call is ambiguous between the following methods or properties: 'R.R(R)' and 'R.R(R)' | ||
// public Derived(Derived y) : base(y) => throw null; // 3, 4, 5 | ||
Diagnostic(ErrorCode.ERR_AmbigCall, "base").WithArguments("R.R(R)", "R.R(R)").WithLocation(8, 33), | ||
// (8,33): error CS8868: A copy constructor in a record must call a copy constructor of the base, or a parameterless object constructor if the record inherits from object. |
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.
// (8,33): error CS8868: A copy constructor in a record must call a copy constructor of the base, or a parameterless object constructor if the record inherits from object. [](start = 16, length = 171)
Should we suppress this error, it feels like it is cascading?
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 4)
@@ -3540,7 +3549,8 @@ private void AddSynthesizedConstructorsIfNecessary(ArrayBuilder<Symbol> members, | |||
{ | |||
case MethodKind.Constructor: | |||
// Ignore the record copy constructor | |||
if (!IsRecord || !SynthesizedRecordCopyCtor.HasCopyConstructorSignature(method)) | |||
if (!IsRecord || | |||
!(SynthesizedRecordCopyCtor.HasCopyConstructorSignature(method) && method is not SynthesizedRecordConstructor)) |
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.
Somehow, my brain just can't work out the exact purpose or meaning of this expression. When I apply De Morgan's to it (which I think makes it a little easier to understand) I get:
!IsRecord
|| !SynthesizedRecordCopyCtor.HasCopyConstructorSignature(method)
|| method is SynthesizedRecordConstructor
So we use a constructor as evidence to skip the implicit parameterless constructor if:
- it's not contained in a record
- it doesn't have the copy constructor signature, or
- it is a synthesized record constructor.
I wasn't sure why (3) is necessary. If the record primary constructor gets skipped because it matches the copy constructor signature, isn't that fine, since it's an error scenario? #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.
If the record primary constructor gets skipped because it matches the copy constructor signature, isn't that fine, since it's an error scenario?
No, it is not fine. We know it is not a copy constructor. We know it is a constructor explicitly declared in code, therefore, its presence should suppress declaration of a parameter-less constructor.
In reply to: 544673679 [](ancestors = 544673679)
@@ -6591,4 +6591,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ | |||
<data name="WRN_UnreadRecordParameter_Title" xml:space="preserve"> | |||
<value>Parameter is unread. Did you forget to use it to initialize the property with that name?</value> | |||
</data> | |||
<data name="ERR_RecordAmbigCtor" xml:space="preserve"> | |||
<value>Synthesized copy constructor conflicts with a primary constructor.</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.
nit: consider wording the message as something like "Primary constructor is not allowed to use the copy constructor signature 'Rec(Rec)'", for example. 'Rec(Rec)' could be given by passing the copy constructor symbol as an argument to the diagnostic.
The primary constructor is the bit that needs to change, so should be the first to be mentioned, and showing the SymbolDisplay I think provides a bit of a hint as to what about the primary constructor needs to change. #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.
Makes sense to mention the primary constructor first. Adjusted
In reply to: 544674566 [](ancestors = 544674566)
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 with some nits/suggestions
Fixes #49628
Spec change dotnet/csharplang#4234