-
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
Avoid AmbigMember in lookup of interfaces with nullability differences only #49338
Conversation
9714d4d
to
2eb4242
Compare
At a glance it feels like no to me. This is a bug-fix level change, especially since in old compilers use of the |
Agree. Don't gate on |
@@ -1148,6 +1148,10 @@ private static void MergeHidingLookupResults(LookupResult resultHiding, LookupRe | |||
// SPEC: interface type, the base types of T are the base interfaces | |||
// SPEC: of T and the class type object. | |||
|
|||
if (hiddenContainer.Equals(hidingSym.ContainingType, TypeCompareKind.IgnoreNullableModifiersForReferenceTypes)) |
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 my eductation: can we get into a similar problem when interfaces differ by object
and dynamic
type parameters? #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.
yes, but dynamic
is not allowed as a type argument. See AmbigMember_DynamicDifferences
below. #Closed
@@ -1148,6 +1148,10 @@ private static void MergeHidingLookupResults(LookupResult resultHiding, LookupRe | |||
// SPEC: interface type, the base types of T are the base interfaces | |||
// SPEC: of T and the class type object. | |||
|
|||
if (hiddenContainer.Equals(hidingSym.ContainingType, TypeCompareKind.IgnoreNullableModifiersForReferenceTypes)) | |||
{ | |||
goto symIsHidden; // discard the new candidate, as it is not really different |
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.
goto symIsHidden [](start = 32, length = 16)
How do we know it is the "same" member? We are not looking at signature, etc. For example, in the if
below that is also deals with "hiding" we don't make this assumption. I am assuming that we are "resolving" that "hiding" elsewhere. It feels like the new hiding should be resolved there as well. Meaning that we simply add negated if
condition to the if
below and add additional code elsewhere to handle methods and indexers. #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 looks like alternatively we could filter out "duplicate" interfaces inside ```GetBaseInterfaces```` helper.
In reply to: 524690053 [](ancestors = 524690053)
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 don't think we can filter in GetBaseInterfaces
because that would affect IsDerivedType
which also uses that helper, but we could do the filtering at the call-site of GetBaseInterfaces
in LookupMembersInInterfaceOnly
.
I had done that in drafting this PR, but changed to this approach to reduce allocations and iterations.
Do you have a preference?
I didn't fully understand your question above. We can discuss tomorrow.
In reply to: 524709873 [](ancestors = 524709873,524690053)
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 don't think we can filter in GetBaseInterfaces because that would affect IsDerivedType which also uses that helper
That is a fair point. Then it looks like we could filter interfaces in LookupMembersInInterfacesWithoutInheritance
. Why bother even performing a lookup if we already looked in a "similar" interface and planning to discard everything we find?
I didn't fully understand your question above.
C# hides by signature. Signature is part of detecting if the candidate is different (as the comment says). Where do we check the signature if we are saying "as it is not really different"? If we don't check the signature, we shouldn't claim that "it is not really different".
In reply to: 542952838 [](ancestors = 542952838,524709873,524690053)
} | ||
"; | ||
var comp = CreateCompilation(src); | ||
comp.VerifyDiagnostics(); |
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.
comp.VerifyDiagnostics(); [](start = 12, length = 25)
In this and other tests that are supposed to verify that an ambiguity is successfully resolved, I think we should verify how exactly it is resolved to ensure that the resolution is deterministic, etc. #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 verifying this in two ways: the behavior of .Item
during nullability analysis, and by checking how the ambiguity is resolved in AllInterfaces
.
I could also check GetSymbolInfo
on i.Item
, but that doesn't seem to add much. Is that what you had in mind?
In reply to: 524699086 [](ancestors = 524699086)
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.
and by checking how the ambiguity is resolved in
AllInterfaces
As far as I can tell, there is no change to the behavior of this API. What could we be checking?
I could also check GetSymbolInfo on i.Item, but that doesn't seem to add much. Is that what you had in mind?
Yes. something like that, I think we need to assert specific symbol. We probably should also test lookup APIs that SemanticModel offers.
In reply to: 542852918 [](ancestors = 542852918,524699086)
Done with review pass (iteration 1) #Closed |
void M(I3 i) | ||
{ | ||
_ = i.Item; | ||
i.Item = 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.
Item [](start = 10, length = 4)
Consider expanding testing to other kinds of members: fields, indexers, events, nested types, methods. #Closed
@@ -1058,11 +1058,21 @@ static void addAllInterfaces(NamedTypeSymbol @interface, HashSet<NamedTypeSymbol | |||
if (interfaces.Length > 0) | |||
{ | |||
var tmp = LookupResult.GetInstance(); | |||
foreach (TypeSymbol baseInterface in interfaces) | |||
HashSet<NamedTypeSymbol> seenInterfaces = 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.
HashSet [](start = 16, length = 7)
Would it make sense to use pooled hash set? #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.
The reason I didn't use the pool is that we'd need a new one for this specific comparer. Not sure that's warranted
In reply to: 544338611 [](ancestors = 544338611)
|
||
foreach (NamedTypeSymbol baseInterface in interfaces) | ||
{ | ||
if (seenInterfaces is null || !seenInterfaces.Contains(baseInterface)) |
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.
seenInterfaces is null || !seenInterfaces.Contains(baseInterface) [](start = 24, length = 65)
seenInterfaces?.Add(baseInterface) != false
and get rid of the Add
inside the if
? #Closed
var tree = comp.SyntaxTrees.Single(); | ||
var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); | ||
var item = tree.GetRoot().DescendantNodes().OfType<MemberAccessExpressionSyntax>().First(); | ||
Assert.True(model.LookupNames(item.SpanStart, i3.GetPublicSymbol()).Contains("Item")); |
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.
.Contains("Item") [](start = 79, length = 17)
I think we actually want to assert in a way that would allow us to verify from which flavor of the interface this symbol actually came from. We want to see is there an ambiguity here and how it is resolved. #Closed
var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); | ||
var item = tree.GetRoot().DescendantNodes().OfType<ElementAccessExpressionSyntax>().First(); | ||
Assert.Equal("i[0]", item.ToString()); | ||
Assert.Equal("System.Object I<System.Object>.this[System.Int32 i] { get; set; }", model.GetSymbolInfo(item).Symbol.ToTestDisplayString()); |
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.
ToTestDisplayString [](start = 127, length = 19)
Should we use TypeWithAnnotations.TestDisplayFormat
here? #Closed
var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); | ||
var item = tree.GetRoot().DescendantNodes().OfType<InvocationExpressionSyntax>().First(); | ||
Assert.Equal("i.Get()", item.ToString()); | ||
Assert.Equal("ref System.Object I<System.Object>.Get()", model.GetSymbolInfo(item).Symbol.ToTestDisplayString()); |
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.
ToTestDisplayString [](start = 102, length = 19)
Should we use TypeWithAnnotations.TestDisplayFormat here? #Closed
Assert.Equal("ref System.Object I<System.Object>.Get()", model.GetSymbolInfo(item).Symbol.ToTestDisplayString()); | ||
|
||
var i3 = comp.GetTypeByMetadataName("I3"); | ||
Assert.True(model.LookupNames(item.SpanStart, i3.GetPublicSymbol()).Contains("Get")); |
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.
Contains("Get") [](start = 80, length = 15)
Similar comment as for the other test above. #Closed
|
||
public class C | ||
{ | ||
void M(I3 i) |
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.
void M(I3 i) [](start = 4, length = 12)
I think we should also test lookup in a type parameter. I.e. when i
is of type T
, which has I3
as a constraint. #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 we should also test lookup in a type parameter. I.e. when i is of type T, which has I3 as a constraint.
This comment is for scenarios involving the nullability mismatch. Also, I think we should test scenarios when constraints are just I<object>, I2<object>
and I2<object>, I<object>
.
In reply to: 544360179 [](ancestors = 544360179)
var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); | ||
var item = tree.GetRoot().DescendantNodes().OfType<MemberAccessExpressionSyntax>().First(); | ||
Assert.Equal("i.Item", item.ToString()); | ||
Assert.Equal("System.Object I<System.Object>.Item { get; set; }", model.GetSymbolInfo(item).Symbol.ToTestDisplayString()); |
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.
ToTestDisplayString [](start = 111, length = 19)
TypeWithAnnotations.TestDisplayFormat? #Closed
interface I2<T> : I<T> where T : class { } | ||
|
||
#nullable disable | ||
interface I3 : I<object>, I2<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.
I, I2 [](start = 15, length = 21)
Does flipping the order here change what symbols we get? #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 does. See AmbigMember_DifferenceBetweenNonnullableAndOblivious_WithConstraint_ReverseOrder
In reply to: 544374723 [](ancestors = 544374723)
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 does. See AmbigMember_DifferenceBetweenNonnullableAndOblivious_WithConstraint_ReverseOrder
It doesn't look like we assert symbol in that test.
In reply to: 544603988 [](ancestors = 544603988,544374723)
Done with review pass (commit 4) #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 (commit 5), assuming CI is passing.
@dotnet/roslyn-compiler for second review. Thanks |
{ | ||
return symbols.Select(s => s.ToTestDisplayString()).ToArray(); | ||
format ??= SymbolDisplayFormat.TestFormat; |
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 doing something like the following here:
return symbols.SelectAsArray(static (s, format) => s.ToDisplayString(format), format);
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 leave as-is to get this into preview3. Thanks
Scenarios where an interface is implemented twice with nullability differences result in lookup errors.
See example below. My PR on type substitution (#48694) would make this more common (wouldn't require the
class
constraint) and ran into this error during bootstrap build.The general desire for the nullability feature is that it should only produce warnings.
Options:
TypeWithAnnotations.MergeEquivalentTypes
), so thatAllInterfaces
wouldn't contain such duplicatesGetBaseInterfaces
inBinder_Lookup.cs
)From discussion with Aleksey, option 3 seems the safest, as it does not create types that didn't exist. It's possible that parts of the code base assume that
.Interfaces
is a subset of.AllInterfaces
, for instance, or that members found during lookup on an interface belong to that interface or its bases.Open question: should we gate this fix behing LangVer in some way?