Properly treat ambiguous explicit interface implementations#34584
Conversation
… nullable reference types, including maintaining backwards compatability with pre -NRT code.
|
@YairHalberstadt |
|
@AlekseyTs |
…lision diagnostics.
src/Compilers/CSharp/Portable/Symbols/MemberSignatureComparer.cs
Outdated
Show resolved
Hide resolved
|
@YairHalberstadt Please see dotnet/csharplang#2378 (comment) . I know you're aware of that because you responded there, but I wanted to close the loop here. Could you please modify this PR to do "step 1" to fix the compatibility issue? #Resolved |
|
Sure. I think it's worth keeping the change where we check for compile time ambiguities during the lookup, rather than afterwards, since this is more correct, and would have helped diagnose this breaking change earlier. #Resolved |
Only make one pass in explicitImplementationHelpers
|
Obviously this fix has broken a large number of unit tests. Depending on the intent of the test I've tried to do one of these things:
However it has been tricky to know exactly which one is best, and it's likely a few tests will need changing. I'm now pushing my changes to here, to see which other tests fail. I'm sure there will be a few. #Resolved |
|
It looks like all the tests are now passing. |
gafter
left a comment
There was a problem hiding this comment.
LGTM. @AlekseyTs Can you please have a look?
| } | ||
|
|
||
| [Fact] | ||
| [Fact(Skip = "https://github.com/dotnet/csharplang/issues/2378#issuecomment-479634969")] |
There was a problem hiding this comment.
[Fact(Skip = "https://github.com/dotnet/csharplang/issues/2378#issuecomment-479634969")] [](start = 8, length = 88)
I think skipping tests doesn't help us to understand what would be the behavior after this PR. I would suggest to unskip the tests that existed prior to this PR and undo any modifications to the scenarios (like addition of constraints etc.). It is OK to add new skipped tests, but they should be linked to an active compiler issue (not a csharplang issue). #Closed
|
|
||
| for (int i = arity - 1; i >= 0; i--) | ||
| { | ||
| builder.Add(IndexedTypeParameterSymbolForOverriding.GetTypeParameter(i, typeParameters2[i].IsValueType)); |
There was a problem hiding this comment.
IndexedTypeParameterSymbolForOverriding.GetTypeParameter [](start = 32, length = 56)
It looks like we can remove the IndexedTypeParameterSymbolForOverriding type. #Closed
| return makeNullableT(); | ||
| } | ||
|
|
||
| if (typeParameterSymbol.DeclaringMethod is {} declaringMethod && (declaringMethod.IsOverride || declaringMethod.IsExplicitInterfaceImplementation)) |
There was a problem hiding this comment.
if (typeParameterSymbol.DeclaringMethod is {} declaringMethod && (declaringMethod.IsOverride || declaringMethod.IsExplicitInterfaceImplementation)) [](start = 12, length = 147)
This condition should be applicable only to type references in a signature of a method declaration, but it looks like this isn't the case. #Closed
There was a problem hiding this comment.
For example, what would be the type of the local in this scenario (please add a test)?
interface I5
{
void M5<T>(T value) where T : class;
}
class C51 : I5
{
void I5.M5<T>(T value)
{
T? x = value;
}
}
I think the more robust approach would be to force the type from the Binder, if the Binder knows that it binds types from the signature. Or, even better, to force the type in method symbol implementation. That would allow us to take a look at the presence of the constraints once we start working on step #2.
In reply to: 273635920 [](ancestors = 273635920)
There was a problem hiding this comment.
The binder doesn't know it binds types from the signature.
As I see it we have three options:
-
When creating the TypeParameterSymbols in SourceOrdianaryMethodSymbol, recursively check the ParameterSyntaxes to see if any TypeParameters are used as NullableTypes.
-
When binding a Type, pass in enough information for the binder to know it is binding types from the signature.
-
When binding a parameter, deep copy the type of the parameter, replacing all lazy nullable types with Nullable Types.
I think 3. is the worst option, as it is both most expensive, and most error prone. I am not sure about 1 or 2, but will go with 1 for now unless you tell me otherwise.
There was a problem hiding this comment.
I would try to do this inside method symbol. Once the return types and parameter types are bound, we can visit them and force lazy nullable types created over method type parameters to resolve towards Nullable<T>. We do not need to replace the TypeWithAnnotations instances. Of course, that would require adding some new APIs to TypeWithAnnotations. We would need to be able to get to the underlying type (__defaultType?) without triggering resolution. And we would need an API that would allow us to resolve the type towards Nullable<T>.
In reply to: 273838023 [](ancestors = 273838023)
There was a problem hiding this comment.
Given a case like this:
interface I
{
void M<T>(T? value) where T : class;
}
class C : I
{
void I.M<T>(T? value)
{
T? x = value;
}
}If we implement your suggestion, we will warn on T? x = value; that T is an unconstrained type.
Is this a problem?
There was a problem hiding this comment.
If we implement your suggestion, we will warn on T? x = value; that T is an unconstrained type.
Is this a problem?
I don't think this is a problem, assuming that we are going to get another error saying the implemented method is not found. I think this is the current behavior for the scenario when I.M is not present (i.e. the interface is empty).
In reply to: 274280058 [](ancestors = 274280058)
|
@AlekseyTs |
Sounds good. #Closed |
Only force type parameters in method signature as nullable structs Revert changes to explicit inteface helper. Changes to unit tests
|
@AlekseyTs One note:
I assumed you used a double underscore there on purpose, so renamed _defaultType to __defaultType. Since it is an implementation detail we are exposing, and is also readonly, I didn't hide it behind a property. |
That actually was a typo. Sorry about that. Since that field is no longer private, it should be renamed to In reply to: 482549072 [](ancestors = 482549072) |
| /// The underlying type, unless overridden by _extensions. | ||
| /// </summary> | ||
| private readonly TypeSymbol _defaultType; | ||
| internal readonly TypeSymbol __defaultType; |
There was a problem hiding this comment.
__defaultType [](start = 37, length = 13)
Names of internal fields should start with a capital letter - DefaultType #Closed
It looks like this is going to do resolution, but it is possible that Refers to: src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs:710 in e260f51. [](commit_id = e260f51, deletion_comment = False) |
| } | ||
|
|
||
| // Any nullable typeParameter declared by the method in the signature of an override or explicit interface implementation is considered a Nullable<T> | ||
| if (IsExplicitInterfaceImplementation || IsOverride) |
There was a problem hiding this comment.
IsExplicitInterfaceImplementation [](start = 16, length = 33)
syntax.ExplicitInterfaceSpecifier != null? #Closed
There was a problem hiding this comment.
I wasn't sure which to use. What is the difference?
There was a problem hiding this comment.
I wasn't sure which to use. What is the difference?
It is good to be consistent in a way this functions checks for certain conditions. For example, below it is using syntax to check if this is an explicit interface implementation. This check will also be faster, eventually the same syntax condition will be checked, I think, but it will jump through more hoops. Etc.
In reply to: 275009353 [](ancestors = 275009353)
| type.TryForceResolveAsNullableValueType(); | ||
| } | ||
| return false; | ||
| }, null, null, false, true); |
There was a problem hiding this comment.
, null, null, false, true [](start = 17, length = 25)
Please use named arguments. #Closed
| { | ||
| type.VisitType<object>(null, (type, unused1, unused2) => | ||
| { | ||
| if (type.DefaultType.IsTypeParameter() && ((TypeParameterSymbol)type.DefaultType).DeclaringMethod != null) |
There was a problem hiding this comment.
type.DefaultType.IsTypeParameter() && ((TypeParameterSymbol)type.DefaultType).DeclaringMethod != null [](start = 24, length = 101)
(type.DefaultType as TypeParameterSymbol)?.DeclaringMethod == (object)this? #Closed
|
@gafter Could you please review the updated PR? |
| bool useDefaultType = false) | ||
| { | ||
| Debug.Assert(typeWithAnnotationsOpt.HasType == (typeOpt is null)); | ||
| Debug.Assert(canDigThroughNullable = false || useDefaultType == false, "digging through nullable will cause early resolution of nullable types"); |
There was a problem hiding this comment.
canDigThroughNullable = false [](start = 25, length = 29)
I think this assignment is not intentional and is the source of the test failures. #Closed
|
I remember jcouv telling me that Foo was banned in the codebase because of spellcheckers, so I've replaced it with Goo in the test files. |
|
@YairHalberstadt Thanks for the contribution! |
* dotnet/master: (495 commits) Roslyn Installer: Stop processes that block VSIX installation. (dotnet#34886) Remove unused helper BeginInvokeOnUIThread Apply a hang mitigating timeout to InvokeOnUIThread Apply a hang mitigating timeout in RestoreNuGetPackages Apply a hang mitigating timeout to WaitForApplicationIdle Fix Value/Ref checks for pattern Index/Range (dotnet#35004) Fix assert in remove unused member analyzer Treat unconstrained type parameters declared in `#nullable disable` context as having an oblivious nullability in case they are substituted with a reference type. (dotnet#34797) Add symbol name to tests. Fix to be the correct name emitted PR feedback Improve IDE0052 diagnostic message for properties with used setter but unused getter Use original definition symbol for fetching control flow graph of generic local functions. Properly treat ambiguous explicit interface implementations (dotnet#34584) Remove the dependence between the order in NullableAnnotation and metadata attribute values (dotnet#34909) Fix warning level test. Fix bootstrap on Linux/Mac (dotnet#34978) disable completion for immediate window commands (dotnet#32631) Updated PreviewWarning color Implement design changes for "pattern" Index/Range indexers (dotnet#34897) Add IVTs to 16.1 version of RemoteLS ...
involving nullable reference types, including maintaining backwards compatability with pre -NRT code.
pre C# 8.0,
void I.Foo<T>(T? value) { }was a valid implementation ofvoid Foo<T>(T? value) where T : struct;.In C# 8 it's a valid implementation of:
This leads to the following 2 issues.
1.
See #34508
The following is a valid, unambiguous program in C# 7.0, but in C# 8.0 leads to an ambiguity which prevents it from compiling:
Given that void I.Foo(T? value) { } is a valid implementation of both
void Foo<T>(T value);andvoid Foo<T>(T? value) where T : struct;the compiler is allowed to decide which it should implement. It does so arbitrarily (by choosing the member declared first by the interface), causing a compile error.To fix this regression I have taken the following approach.
When looking for the implemented member of an explicit interface implementation, The compiler will first look for members of the interface which match the implementing member including nullability annotations. Only if that fails will it look for members whose nullability annotations do not match.
Since no pre-C# 8 code has nullability annotations, this can be guaranteed not to break backwards compatibility.
In my opinion this is an intuitive, simple, and understandable solution, which doesn't require special casing all sorts of potentially backwards compatibility breaking cases.
2.
See dotnet/csharplang#2370
The compiler is required to issue a warning whenever an explicit interface implementation can match two members, or is ambiguous to the runtime. See https://docs.microsoft.com/en-us/dotnet/csharp/misc/cs0473 and https://blogs.msdn.microsoft.com/ericlippert/2006/04/06/odious-ambiguous-overloads-part-two/.
However it was not issuing such an error in this case:
Despite the fact that the explicit interface implementation is ambiguous to the compiler.
This was because the logic for doing the check to see if an explicit interface implementation matches two interface members is done at a different point to the location where the lookup is done. As a result the logic became out of sync. This pull request moves the logic to the same location, so that the warning is guaranteed to be issued accurately.
Note that the lookup to see if a method is ambiguous to the runtime remains in its old location, as it requires completely different logic.