Skip to content
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

Merged
merged 7 commits into from
Dec 18, 2020

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Nov 12, 2020

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:

  1. merge types and their nullabilities (see TypeWithAnnotations.MergeEquivalentTypes), so that AllInterfaces wouldn't contain such duplicates
  2. merges types and their nullabilities for purpose of lookup (see GetBaseInterfaces in Binder_Lookup.cs)
  3. pick a winner whenever such conflicts are detected during lookup

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?

#nullable disable
interface I<T> where T : class { T Item { get; } }

#nullable enable
interface I2<T> : I<T> where T : class { }

#nullable disable
interface I3 : I<object>, I2<object> { }

#nullable enable
public class C
{
    void M(I3 i)
    {
        _ = i.Item; // error CS0229: Ambiguity between 'I<object>.Item' and 'I<object>.Item'
    }
}

@jcouv jcouv self-assigned this Nov 12, 2020
@jcouv jcouv force-pushed the merge-types branch 2 times, most recently from 9714d4d to 2eb4242 Compare November 13, 2020 20:18
@jcouv jcouv changed the title Avoid AmbigMember in lookup of interfaces with nullability differences Avoid AmbigMember in lookup of interfaces with nullability differences only Nov 13, 2020
@jcouv jcouv marked this pull request as ready for review November 13, 2020 22:46
@jcouv jcouv requested a review from a team as a code owner November 13, 2020 22:46
@jcouv jcouv requested a review from AlekseyTs November 13, 2020 22:46
@RikkiGibson
Copy link
Contributor

RikkiGibson commented Nov 13, 2020

Open question: should we gate this fix behing LangVer in some way?

At a glance it feels like no to me. This is a bug-fix level change, especially since in old compilers use of the class constraint will also be needed to get a unification failure.
#Closed

@jaredpar
Copy link
Member

jaredpar commented Nov 16, 2020

Agree. Don't gate on langver #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))
Copy link
Member

@jaredpar jaredpar Nov 16, 2020

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

Copy link
Member Author

@jcouv jcouv Nov 16, 2020

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
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 16, 2020

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

Copy link
Contributor

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)

Copy link
Member Author

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)

Copy link
Contributor

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();
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 16, 2020

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

Copy link
Member Author

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)

Copy link
Contributor

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)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Nov 16, 2020

Done with review pass (iteration 1) #Closed

void M(I3 i)
{
_ = i.Item;
i.Item = null;
Copy link
Contributor

@AlekseyTs AlekseyTs Nov 16, 2020

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;
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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

Copy link
Member Author

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))
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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"));
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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());
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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());
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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"));
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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)
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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

Copy link
Contributor

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());
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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> { }
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 16, 2020

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

Copy link
Member Author

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)

Copy link
Contributor

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)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 16, 2020

Done with review pass (commit 4) #Closed

Copy link
Contributor

@AlekseyTs AlekseyTs left a 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.

@jcouv
Copy link
Member Author

jcouv commented Dec 18, 2020

@dotnet/roslyn-compiler for second review. Thanks

@RikkiGibson RikkiGibson self-assigned this Dec 18, 2020
{
return symbols.Select(s => s.ToTestDisplayString()).ToArray();
format ??= SymbolDisplayFormat.TestFormat;
Copy link
Contributor

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);

Copy link
Member Author

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

@jcouv jcouv merged commit 932e0f1 into dotnet:master Dec 18, 2020
@ghost ghost added this to the Next milestone Dec 18, 2020
@jcouv jcouv deleted the merge-types branch December 18, 2020 23:08
@dibarbet dibarbet modified the milestones: Next, 16.9.P3 Dec 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants