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

Implement Lookup APIs on extensions #72948

Merged
merged 11 commits into from
Apr 29, 2024
Merged

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Apr 9, 2024

... and fix nested generic extension type scenario

Note: only LookupStaticMembers and LookupNamespacesAndTypes are implemented. LookupSymbols will be implemented later (as we need to integrate the lookup for extension methods and extension type members before implementing it).

Relates to test plan #66722

@jcouv jcouv self-assigned this Apr 9, 2024
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged Issues and PRs which have not yet been triaged by a lead label Apr 9, 2024
syntax, right: syntax, diagnostics, ref wasError);
if (!typeArgumentsOpt.IsDefault)
{
namedTypeSymbol = ConstructNamedTypeUnlessTypeArgumentOmitted(syntax, namedTypeSymbol, typeArgumentsSyntax, typeArgumentsOpt, diagnostics);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 This fixes a bug with a generic type in an extension. It's illustrated by ExtensionMemberLookup_MatchingExtendedType_GenericType_GenericMember amongst other tests.

@jcouv jcouv force-pushed the roles-lookup branch 3 times, most recently from b7f9f4b to 8adbfc9 Compare April 16, 2024 23:36
@jcouv jcouv marked this pull request as ready for review April 17, 2024 05:16
@jcouv jcouv requested a review from a team as a code owner April 17, 2024 05:16
@@ -2368,6 +2370,7 @@ private BoundExpression BindNameofOperatorInternal(InvocationExpressionSyntax no
CheckFeatureAvailability(node, MessageID.IDS_FeatureNameof, diagnostics);
var argument = node.ArgumentList.Arguments[0].Expression;
var boundArgument = BindExpression(argument, diagnostics);
boundArgument = ResolveToExtensionMemberIfPossible(boundArgument, diagnostics);
Copy link
Member Author

@jcouv jcouv Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 BindToNaturalType (called below) also does this. I've been debating whether to remove the redundant call. We'll discuss

This problem goes away in my next PR. It will move ResolveToExtensionMemberIfPossible into BindExpression (for non-invocation cases) and remove it from BindToNaturalType. #Resolved

syntax, right: syntax, diagnostics, ref wasError);
if (!typeArgumentsOpt.IsDefault)
{
namedTypeSymbol = ConstructNamedTypeUnlessTypeArgumentOmitted(syntax, namedTypeSymbol, typeArgumentsSyntax, typeArgumentsOpt, diagnostics);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConstructNamedTypeUnlessTypeArgumentOmitted

Are we testing scenario with omitted type arguments (<,>)? We probably should create an unbound generic type in that case. #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.

Yes (see tests with _OmittedTypeArgument). Indeed, we produce a unbound generic symbol then.

syntax, right: syntax, diagnostics, ref wasError);
if (!typeArgumentsOpt.IsDefault)
{
namedTypeSymbol = ConstructNamedTypeUnlessTypeArgumentOmitted(syntax, namedTypeSymbol, typeArgumentsSyntax, typeArgumentsOpt, diagnostics);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namedTypeSymbol

When and where do we check constraints for this type? #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 constraints get checked a couple of layers into ConstructNamedTypeUnlessTypeArgumentOmitted:
ConstructNamedTypeUnlessTypeArgumentOmitted -> ConstructNamedType -> CheckConstraintsForNamedType.
Example test: InferredVariable_TypeReceiver_GenericType_BrokenConstraint

}

[Fact]
public void ExtensionMemberLookup_MatchingExtendedType_GenericType_GenericMember()
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExtensionMemberLookup_MatchingExtendedType_GenericType_GenericMember

Are we testing similar scenario in type-only context? #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.

Add ExtensionMemberLookup_MatchingExtendedType_GenericMember_TypeOnlyContext

internal override void GetCandidateExtensionMethods(
ArrayBuilder<MethodSymbol> methods,
string name,
string? name,
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name

What is the scenario when we don't have a name for an extension method? #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.

LookupSymbols with includeReducedExtensionMethods = true and name = null.

/// </summary>
IncludeExtensionMethods = 1 << 10,
IncludeExtensionMethodsAndMembers = 1 << 10,
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IncludeExtensionMethodsAndMembers

"IncludeExtensionMembers"? #Closed

@@ -54,6 +54,9 @@ public SyntaxNode NameSyntax
}
}

public SeparatedSyntaxList<TypeSyntax> TypeArgumentsSyntax
=> NameSyntax is GenericNameSyntax genericName ? genericName.TypeArgumentList.Arguments : default;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NameSyntax is GenericNameSyntax genericName ? genericName.TypeArgumentList.Arguments : default;

This looks fragile. If we need this syntax, we probably should add it to the BoundMethodGroup as a real property set at construction. #Closed


options |= LookupOptions.AllMethodsOnArityZero;
options &= ~LookupOptions.MustBeInstance;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options &= ~LookupOptions.MustBeInstance;

Why not keep this? It looks more reliable than an assert. #Closed


// We use a Lookup API (which uses a CheckViability check) instead of an AddLookup API (which uses a CanAddLookupSymbolInfo check),
// but that is fine since we filter symbols that cannot be referenced by name below anyways.
scope.Binder.LookupImplicitExtensionMembersInSingleBinder(
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scope.Binder.LookupImplicitExtensionMembersInSingleBinder(

It looks like we are adding to the same lookupResult for all scopes. How is this going to interact with most specific shadowing? #Closed

useSiteInfo.MergeAndClear(ref tempUseSiteInfo);
if (tempUseSiteInfo.AccumulatesDiagnostics)
{
useSiteInfo.MergeAndClear(ref tempUseSiteInfo);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MergeAndClear

It might be better to adjust behavior of MergeAndClear. However, I do not suggest doing this in context of this PR. Consider opening an issue to follow up during quality milestone #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.

Filled #73188

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 18, 2024

            MergeExtensionLookupResultsHidingLessSpecific(result, tempResult, basesBeingResolved, ref useSiteInfo);

How is this going to behave when name is null? #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs:210 in d45a281. [](commit_id = d45a281, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 1), tests are not looked at.

/// <summary>
/// Lookup extension methods by name and arity in the given binder and
/// check viability in this binder. The lookup is performed on a single
/// binder because extension method search stops at the first applicable
/// method group from the nearest enclosing namespace.
/// </summary>
private void LookupExtensionMethodsInSingleBinder(ExtensionScope scope, LookupResult result, string name, int arity, LookupOptions options, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
internal void LookupExtensionMethodsInSingleBinder(ExtensionScope scope, LookupResult result, string? name, int arity, LookupOptions options, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Why is this change necessary? I didn't see any of the call sites changed. #Closed

@@ -925,14 +943,13 @@ internal virtual bool SupportsExtensions
/// </summary>
internal virtual void GetCandidateExtensionMethods(
ArrayBuilder<MethodSymbol> methods,
string name,
string? name,
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Why is this change necessary? I didn't see any of the call sites changed/added #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.

This is annotating the method as currently implemented

// Lookup member in an extension type
private void LookupMembersInExtension(
LookupResult current,
NamedTypeSymbol type,
string name,
string? name,
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string? name,

I think it would be better to not use this method to collect all members for semantic model purposes. I am not confident that it is going to behave reasonably without the name. #Closed

this.AddMemberLookupSymbolsInfoInTypeParameter(result, (TypeParameterSymbol)type, options, originalBinder);
break;

case TypeKind.Interface:
this.AddMemberLookupSymbolsInfoInInterface(result, type, options, originalBinder, type);
accessThroughType ??= type;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessThroughType ??= type;

This looks suspicious. The accessThroughType parameter of AddMemberLookupSymbolsInfoInInterface is used for accessibility check. So, it is not clear to me why would we want to use an extension type for that. #Closed

case TypeKind.Class:
case TypeKind.Struct:
case TypeKind.Enum:
case TypeKind.Delegate:
case TypeKind.Array:
case TypeKind.Dynamic:
case TypeKind.Submission:
this.AddMemberLookupSymbolsInfoInClass(result, type, options, originalBinder, type);
accessThroughType ??= type;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessThroughType ??= type;

Similar comment #Closed


if (type.ExtendedTypeNoUseSiteDiagnostics is { } extendedType)
{
AddMemberLookupSymbolsInfoInType(result, extendedType, options, originalBinder, accessThroughType: type);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessThroughType: type

Similar comment. It is not obvious to me that this is the right thing to do. #Closed

this.AddMemberLookupSymbolsInfoInTypeParameter(result, (TypeParameterSymbol)type, options, originalBinder);
break;

case TypeKind.Interface:
this.AddMemberLookupSymbolsInfoInInterface(result, type, options, originalBinder, type);
accessThroughType ??= type;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessThroughType ??= type

Do we actually need to assign? Can we simply pass accessThroughType ?? type as the argument below? #Closed

/// <summary>
/// Lookup extension methods by name and arity in the given binder and
/// check viability in this binder. The lookup is performed on a single
/// binder because extension method search stops at the first applicable
/// method group from the nearest enclosing namespace.
/// </summary>
private void LookupExtensionMethodsInSingleBinder(ExtensionScope scope, LookupResult result, string name, int arity, LookupOptions options, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
internal void LookupExtensionMethodsInSingleBinder(ExtensionScope scope, LookupResult result, string? name, int arity, LookupOptions options, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal

Is accessibility change necessary? I do not see any new call sites #Closed

internal void AddImplicitExtensionMemberLookupSymbolsInfoForType(LookupSymbolsInfo result, TypeSymbol type, LookupOptions options, Binder originalBinder)
{
var accessThroughType = type;
if (type.ExtendedTypeNoUseSiteDiagnostics is { } extendedType)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (type.ExtendedTypeNoUseSiteDiagnostics is { } extendedType)

I am not sure why are we continuing otherwise. Could you elaborate, please. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps even a better question: "Why would we ever call this method with an extension type?"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extension types get the benefit of members from other extension types too.

class C { }
implicit extension E for C { }
implicit extension E2 for C { void Member() { } }

When trying to list all the extension members that are applicable to type E, we should find E2.Member.
Note: we also do this during binding (in LookupImplicitExtensionMembersInSingleBinder).

options |= LookupOptions.AllMethodsOnArityZero;
options &= ~LookupOptions.MustBeInstance;
// PROTOTYPE(static) confirm that we want to exclude type parameters from extension member resolution or leave a comment
if (container is TypeSymbol typeSymbol && !typeSymbol.IsTypeParameter())
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (container is TypeSymbol typeSymbol && !typeSymbol.IsTypeParameter())

Should we check that container is not an extension? Are we looking for extensions on an extension in regular binding scenarios? #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 26, 2024

internal static class TypeUnification

Are changes in this file related to lookup changes? #Closed


Refers to: src/Compilers/CSharp/Portable/Symbols/TypeUnification.cs:14 in 90e907a. [](commit_id = 90e907a, deletion_comment = False)


internal void AddImplicitExtensionMemberLookupSymbolsInfoForType(LookupSymbolsInfo result, TypeSymbol type, LookupOptions options, Binder originalBinder)
{
var accessThroughType = type;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var accessThroughType = type;

Do we have tests demonstrating impact of this value? #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.

No, I don't think this will be observable until we have inheritance in extensions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think this will be observable until we have inheritance in extensions

Are protected API declared in an extension accessible through extended type today? If they are not, then passing this accessThroughType value might make a difference.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not. Added a Lookup test for this and there was no observable impact


void addMemberLookupSymbolsInfoInExtension(LookupSymbolsInfo result, TypeSymbol type, LookupOptions options, Binder originalBinder)
{
AddMemberLookupSymbolsInfoWithoutInheritance(result, type, options, originalBinder, accessThroughType: type);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessThroughType: type

Do we have tests demonstrating impact of this value? #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.

No, I don't think this will be observable until we have inheritance in extensions


if (type.ExtendedTypeNoUseSiteDiagnostics is { } extendedType)
{
AddMemberLookupSymbolsInfoInType(result, extendedType, options, originalBinder, accessThroughType: type);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessThroughType: type

Do we have tests demonstrating impact of this value? #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.

Yes, Lookup_UnderlyingTypeMembers_Private and Lookup_UnderlyingTypeMembers_Protected

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 8)

@jcouv
Copy link
Member Author

jcouv commented Apr 26, 2024

internal static class TypeUnification

The PR description also includes a fix for nested generic extension type scenario, which includes the change here and the handling of type arguments in GetExtensionMemberAccess.


In reply to: 2079830450


Refers to: src/Compilers/CSharp/Portable/Symbols/TypeUnification.cs:14 in 90e907a. [](commit_id = 90e907a, deletion_comment = False)

@jcouv jcouv requested a review from AlekseyTs April 26, 2024 20:07

foreach (NamedTypeSymbol extension in compatibleExtensions)
{
AddMemberLookupSymbolsInfoWithoutInheritance(result, extension, options, scope.Binder, accessThroughType: null);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null

Even if the difference is unobservable right now, I think we were passing the right value, given the meaning of the parameter, #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.

Restored. Thanks


void addMemberLookupSymbolsInfoInExtension(LookupSymbolsInfo result, TypeSymbol type, LookupOptions options, Binder originalBinder)
{
AddMemberLookupSymbolsInfoWithoutInheritance(result, type, options, originalBinder, accessThroughType: null);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null

Even if the difference is unobservable right now, I think we were passing the right value, given the meaning of the parameter #Closed

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 10)

@jcouv jcouv requested a review from AlekseyTs April 27, 2024 01:07
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 11)

@jcouv jcouv requested a review from jjonescz April 27, 2024 01:53
@jcouv jcouv merged commit a33cedf into dotnet:features/roles Apr 29, 2024
24 checks passed
@jcouv jcouv deleted the roles-lookup branch April 29, 2024 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers New Language Feature - Roles Roles untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants