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
Resolve extension members in all non-invocation contexts #73239
Conversation
b1d86df
to
c4afd43
Compare
📝 I will address this in my next PR. The issue is that the sorting helper used in Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:31130 in c4afd43. [](commit_id = c4afd43, deletion_comment = False) |
// All other contexts can go ahead and resolve to a non-method extension member | ||
if (!invoked) | ||
{ | ||
result = ResolveToNonMethodExtensionMemberIfPossible(result, diagnostics); |
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.
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.
Maybe I didn't quite understand the question. ResolveToNonMethodExtensionMemberIfPossible
only returns non-methods.
I think the code in Also, I already have a note in the test plan that we need to exclude extension type methods in some collection expression or params scenarios. In reply to: 2085924348 Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs:1127 in bba7662. [](commit_id = bba7662, deletion_comment = False) |
@@ -7560,7 +7543,7 @@ private BoundExpression ResolveToExtensionMemberIfPossible(BoundExpression expr, | |||
} | |||
} | |||
|
|||
private BoundExpression BindInstanceMemberAccess( | |||
private BoundExpression BindMemberAccessWithBoundLeftInternal( |
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 are we trying to convey with the "Internal" suffix? It doesn't look meaningful to me. Consider dropping it. If your goal is as simple as to not share name with the other helper, numeric suffixes "BindMemberAccessWithBoundLeft1" and "BindMemberAccessWithBoundLeft2" will be better in my opinion. #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, my main goal is to remove the Instance
portion (misleading) and not share names since we have a cref reference to it. How about "Core" suffix?
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.
How about "Core" suffix?
I do not find this better. A numeric suffix is better in my opinion to avoid sharing the name. Unless we can come up with a meaningful differentiator.
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.
Since you already changed the name, I guess I can live with it.
|
||
if (!invoked) | ||
{ | ||
var extensionResult = ResolveToNonMethodExtensionMemberIfPossible(result, diagnostics); |
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.
@@ -721,14 +721,15 @@ private bool HasApplicableMemberWithPossiblyExpandedNonArrayParamsCollection<TMe | |||
(analyzedArguments.HasDynamicArgument ? OverloadResolution.Options.DynamicResolution : OverloadResolution.Options.None)); | |||
diagnostics.Add(expression, useSiteInfo); | |||
|
|||
if (resolution.IsExtensionMember(out Symbol extensionMember)) | |||
if (resolution.IsNonMethodExtensionMember(out Symbol extensionMember)) |
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.
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, we can still get here. Below is an example.
When we have an invocation, we delay the resolution of extensions when binding a member access. We resolve the extensions when binding the invocation (ie. here). So we may find that our receiver is not a method group, but instead an invocable member (for example a delegate type field).
In such case, we bind that member access to the invocable member and invoke it with the given arguments.
public void ExtensionInvocation_OnlyDelegateFieldExists()
{
// Invocable fields are considered during extension invocation
var source = """
E.Field = (i) => { System.Console.Write($"ran({i})"); };
C.Field(42);
class C { }
delegate void D(int i);
implicit extension E for C
{
public static D Field;
}
""";
I asked the question because I saw that In reply to: 2086545538 Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs:1127 in bba7662. [](commit_id = bba7662, deletion_comment = False) |
@jjonescz for second review. Thanks |
Thanks. I identified a scenario that hits that. Blocked it off for now and left a PROTOTYPE comment In reply to: 2088772543 Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs:1127 in bba7662. [](commit_id = bba7662, deletion_comment = False) |
I am not sure what are you suggesting to allow in this scenario. Especially that the error isn't "near" a collection expression. #Closed Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:37490 in f33ffce. [](commit_id = f33ffce, deletion_comment = False) |
I would expect an error here, there is no suitable Add method. It must be a method, extensions do not change anything about this requirement. #Closed Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:37477 in f33ffce. [](commit_id = f33ffce, deletion_comment = False) |
@@ -953,25 +955,6 @@ private static UnboundLambda MakeQueryUnboundLambda(CSharpSyntaxNode node, Query | |||
// Could not find an implementation of the query pattern for source type '{0}'. '{1}' not found. | |||
diagnostics.Add(ErrorCode.ERR_QueryNoProvider, node.Location, MessageID.IDS_AnonMethod.Localize(), methodName); | |||
} | |||
else if (ultimateReceiver.Kind == BoundKind.MethodGroup) |
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.
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.
This code is unreachable. The case above for ultimateReceiver.HasAnyErrors
intercepts all the cases that might be handled here.
The reason I prefer to remove the code rather than just leave it is that it calls ResolveMethodGroup
which begs the question for how extensions should be handled. But anything I do here cannot be tested.
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.
This code is unreachable. The case above for ultimateReceiver.HasAnyErrors intercepts all the cases that might be handled here.
Thanks for the explanation. I am good with removal of the code.
Just an FYI, I am done reviewing compiler changes for commit 6. Planning to review tests later today. |
flags: BoundMethodGroupFlags.SearchExtensionMethods, node, typeArgumentsSyntax, diagnostics); | ||
|
||
if (!invoked) |
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.
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 need arguments to resolve invocation scenarios, even if we're only interested in invocable non-method members.
Consider a classic extension method in an inner scope and an extension type with an invocable non-method member in an outer scope.
Depending on whether the extension method is applicable or not, the invocable non-method member may be found or not.
But to determine whether it is applicable, we need arguments.
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 offline discussion, it would be possible to do this unconditionally, but it would not always perform the resolution to a non-extension member (sometimes that will be possible to do only once we have arguments to discard some extension methods that are in closer scopes) and it would probably not be more readable/understandable.
flags, node, typeArgumentsSyntax, diagnostics); | ||
|
||
if (!invoked) |
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 feels like there are way too many tests added given the small amount of actual code paths modified. Of course, having more tests doesn't hurt. However, consider if there is an excessive redundancy given the final state of the changes in this PR, and, perhaps, some scenarios no longer add real value. #Closed Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:39614 in 0b41777. [](commit_id = 0b41777, deletion_comment = False) |
Done with review pass (commit 8) |
Makes sense. I pruned some tests that were more relevant for a previous implementation (all scenarios calling In reply to: 2089257969 Refers to: src/Compilers/CSharp/Test/Emit3/ExtensionTypeTests.cs:39614 in 0b41777. [](commit_id = 0b41777, deletion_comment = False) |
// or than any method from an extension type), then that's the member being accessed. | ||
// | ||
// - if the extension member lookup finds a method (classic extension method compatible with the receiver or method in extension type; | ||
// closer than any non-method extension member), we don't need to touch the result for the member access (it's a method group already). |
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.
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.
My intent is to add some documentation that relates to the callers (which are unfortunately split across two different methods). I'll try to clarify comment
// | ||
// - if the extension member lookup is ambiguous, then we'll use an error symbol as the result of the member access. | ||
// | ||
// - if the extension member lookup finds nothing, then we don't need to touch the result for the member access. |
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 expr; | ||
if (!memberAccess.HasErrors && typeArgumentsSyntax.Any(SyntaxKind.OmittedTypeArgument)) | ||
{ | ||
Error(diagnostics, ErrorCode.ERR_OmittedTypeArgument, syntax); |
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.
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, ExtensionMemberLookup_MatchingExtendedType_GenericType_GenericMember_OmittedTypeArgument
(a generic type scenario) and ExtensionInvocation_TypeReceiver_TypeArguments_Omitted
and ExtensionInvocation_InstanceReceiver_TypeArguments_Omitted
(a generic method, with either a type or an instance receiver).
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 12)
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 13)
When we bind a member access expression there are two cases:
invocation_expression
), in which case the extension member lookup will filter out non-invocable members,This PR implements the second case.
It also removes previous logic we had for resolving to a non-method extension member during a conversion. That logic relied on an incorrect understanding of the spec (that conversions to a delegate type would cause the expression to be treated as "invoked", so we had to wait until processing the conversion to perform the extension member lookup).
So the following scenario becomes an error:
Here are the relevant sections of the spec that justify this understanding:
The Member Lookup has concept of a member being "invoked":
But in Method group conversions, although treat the conversion as an invocation:
The method group conversion only kicks in when we have already determined that we have a method group:
In short, a conversion to a delegate type does not cause the expression to be treated as "invoked", although we process it somewhat like an invocation.
Relates to test plan #66722