-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Extensions: interceptors #79010
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
Extensions: interceptors #79010
Conversation
6469d19
to
34c2420
Compare
} | ||
"""; | ||
var comp = CreateCompilation([source, interceptors, s_attributesSource], parseOptions: RegularNextWithInterceptors); | ||
comp.VerifyEmitDiagnostics(); |
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.
Should we verify execution to see that the interception succeeded?
Applies to other new tests that don't have compilation errors. #Resolved
|
||
static ParameterSymbol? getReceiverParameter(MethodSymbol method, bool classicExtensionNotInvokedAsStatic) | ||
{ | ||
if (method.IsExtensionMethod && classicExtensionNotInvokedAsStatic) |
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.
Do we have tests for extension methods that are invoked as static?
interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null; | ||
switch (methodThisParameter, interceptorThisParameterForCompare) | ||
ParameterSymbol? methodReceiverParameter = getReceiverParameter(method, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod); | ||
ParameterSymbol? interceptorThisParameterForCompare = getReceiverParameter(interceptor, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod || needToReduceInterceptor); |
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.
Why does invokedAsExtensionMethod
play a role in determining the receiver parameter of the interceptor? #Resolved
@@ -141,6 +141,7 @@ private void InterceptCallAndAdjustArguments( | |||
bool invokedAsExtensionMethod, | |||
Syntax.SimpleNameSyntax? nameSyntax) | |||
{ | |||
// Note: the extension method rewriter comes after the local rewriter, so we're still dealing with skeleton methods here |
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.
|
||
if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare)) | ||
MethodSymbol? interceptorForCompare = | ||
interceptor.GetIsNewExtensionMember() && invokedAsExtensionMethod ? interceptor.TryGetCorrespondingExtensionImplementationMethod() : |
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.
|
||
if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare)) | ||
MethodSymbol? interceptorForCompare = | ||
interceptor.GetIsNewExtensionMember() && invokedAsExtensionMethod ? interceptor.TryGetCorrespondingExtensionImplementationMethod() : |
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.
var interceptorThisParameterForCompare = needToReduce ? interceptor.Parameters[0] : | ||
interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null; | ||
switch (methodThisParameter, interceptorThisParameterForCompare) | ||
ParameterSymbol? methodReceiverParameter = getReceiverParameter(method, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod); |
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.
var interceptorThisParameterForCompare = needToReduce ? interceptor.Parameters[0] : | ||
interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null; | ||
switch (methodThisParameter, interceptorThisParameterForCompare) | ||
ParameterSymbol? methodReceiverParameter = getReceiverParameter(method, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod); |
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 value feels wrong. methodThisParameter
didn't depend on invokedAsExtensionMethod
before and the renamed value methodReceiverParameter
shouldn't depend on it too, meaning we should always pass false
here. Otherwise, it looks like there is a behavior change here for a legacy scenario, however, there is no indication (in a comment or in a PR description) that there is an intent to fix a bug. #Closed
interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null; | ||
switch (methodThisParameter, interceptorThisParameterForCompare) | ||
ParameterSymbol? methodReceiverParameter = getReceiverParameter(method, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod); | ||
ParameterSymbol? interceptorThisParameterForCompare = getReceiverParameter(interceptor, classicExtensionNotInvokedAsStatic: invokedAsExtensionMethod || needToReduceInterceptor); |
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.
Is this going to do the right thing for a new extension method? #Closed Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs:175 in 34c2420. [](commit_id = 34c2420, deletion_comment = False) |
if (!argumentRefKindsOpt.IsDefault) | ||
{ | ||
argumentRefKindsOpt = argumentRefKindsOpt.Insert(0, thisRefKind); | ||
} | ||
} | ||
} | ||
|
||
method = interceptor; |
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.
Let's say interceptor
is an instance extension and method
is invoked as extension. It looks like we are going to use implementation form of interceptor
to match the signature to method
. So far good. But then here we are replacing method
with interceptor
(the declaration form). How are they going to align in the BoundCall that will be created. There would be no receiver and too many arguments in that call. I am failing to connect the dots. What am I missing? #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 constructing a busted BoundCall from lowering (lacks receiver, has extra argument) but the ExtensionMethodReferenceRewriter
hid the problem. I'll add an assertion there. Thanks for catching this
It is not clear what guarantees this for new extensions. And it is not clear why would we want to have this restriction for new extensions, given that implementation methods necessary satisfy the restriction. BTW, it looks like we don't have tests for generic extension blocks. #Closed Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs:153 in 34c2420. [](commit_id = 34c2420, deletion_comment = False) |
@@ -141,6 +141,7 @@ private void InterceptCallAndAdjustArguments( | |||
bool invokedAsExtensionMethod, | |||
Syntax.SimpleNameSyntax? nameSyntax) | |||
{ | |||
// Note: the extension method rewriter comes after the local rewriter, so we're still dealing with skeleton methods here | |||
if (this._compilation.TryGetInterceptor(nameSyntax) is not var (attributeLocation, interceptor)) |
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.
if (this._compilation.TryGetInterceptor(nameSyntax) is not var (attributeLocation, interceptor))
This function went through some rather complicated changes with no explanation why the logic should be this way or that way. I am guessing that the complexity is coming from a desire to reconcile an interception of a classic extension invocation by a new style instance extension method. I am suggesting to use an alternative implementation strategy. Starting with the following:
- Revert all the changes
- Right after this line succeeds, if
interceptor
is a new extension, replace it with implementation form. There is no taboo against using implementation forms during lowering. Especially in a case like this, where we are not dealing with a method explicitly referenced by a user at the call site. - Adjust
method.TryGetThisParameter(out var methodThisParameter);
to get extension parameter for instance extensions. - Let the rest of the method take its course and do what it used to do.
If that doesn't address some scenarios, I will be happy to discuss them and suggest appropriate adjustments to the strategy. #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.
Thanks. That approach is much simpler indeed :-) I'd stuck myself with constraint of leaving the ExtensionMethodReferenceRewriter deal with replacement :-/
Done with review pass (commit 1), test are not reviewed. |
@@ -73,6 +73,8 @@ public static BoundNode VisitCall(BoundTreeRewriter rewriter, BoundCall node) | |||
|
|||
static BoundExpression visitArgumentsAndFinishRewrite(BoundTreeRewriter rewriter, BoundCall node, BoundExpression? rewrittenReceiver) | |||
{ | |||
Debug.Assert(!node.Method.GetIsNewExtensionMember() || node.Method.IsStatic || node.ReceiverOpt is not 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.
@@ -147,6 +147,11 @@ private void InterceptCallAndAdjustArguments( | |||
return; | |||
} | |||
|
|||
if (interceptor.GetIsNewExtensionMember() && interceptor.TryGetCorrespondingExtensionImplementationMethod() is { } implementationMethod) |
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.
interceptor.TryGetCorrespondingExtensionImplementationMethod() is { } implementationMethod
It doesn't feel like we can continue otherwise. If there is a guarantee that the implementation method is present (for example, it is always defined in the same source module), then we should throw unreachable. If there is no guarantee, then we should rep[ort an error and recover. #Closed
} | ||
"""; | ||
var comp = CreateCompilation([source, interceptors, s_attributesSource], parseOptions: RegularPreviewWithInterceptors); | ||
comp.VerifyEmitDiagnostics( |
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.
Done with review pass (commit 2) |
@@ -73,6 +73,8 @@ public static BoundNode VisitCall(BoundTreeRewriter rewriter, BoundCall node) | |||
|
|||
static BoundExpression visitArgumentsAndFinishRewrite(BoundTreeRewriter rewriter, BoundCall node, BoundExpression? rewrittenReceiver) | |||
{ | |||
Debug.Assert(node.Method.MethodKind != MethodKind.Ordinary || node.Method.IsStatic || node.ReceiverOpt is not 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.
Done with review pass (commit 3) |
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 4)
Relates to test plan #76130