Extensions: nullability analysis for invocations and method groups#78080
Extensions: nullability analysis for invocations and method groups#78080jcouv merged 9 commits intodotnet:mainfrom
Conversation
27ea047 to
44cd555
Compare
44cd555 to
2f2f70d
Compare
|
@jjonescz @AlekseyTs for review. Thanks |
| in CheckConstraintsArgs args) | ||
| { | ||
| if (!RequiresChecking(method)) | ||
| Debug.Assert(!method.GetIsNewExtensionMember()); // Tracked by https://github.com/dotnet/roslyn/issues/76130: Review all remaining callers with regards to new extensions |
There was a problem hiding this comment.
The question was whether we want any of the callers be affected by extensions. If not, they'll continue to use this method. Otherwise, their logic needs to be updated (lookup logic) and they should call CheckConstraintsIncludingExtension.
After reviewing the callers, I see that none of them accounts for classic extension methods, so I don't think they should be affected by new extension methods either.
Will remove this follow-up comment
There was a problem hiding this comment.
I am sorry, but the response doesn't make sense to me. We don't want the callers to be affected , but we call CheckConstraintsIncludingExtension here. So, why is it a good thing to have two methods after you reviewed all callers?
There was a problem hiding this comment.
Resolved offline. Brought back original name
| skipParameters); | ||
| } | ||
|
|
||
| public static bool CheckMethodConstraintsIncludingExtension( |
There was a problem hiding this comment.
We discussed offline that we probably want to keep the original name of the method and push the logic that checks constraints on the extension down to the "root" CheckConstraints helper (so that no one could miss that logic no matter what overload is used).
| { | ||
| return method.GetIsNewExtensionMember() | ||
| ? method.ContainingType.TypeParameters.Concat(method.TypeParameters) | ||
| : method.ConstructedFrom.TypeParameters; |
|
|
||
| if (member is PropertySymbol property) | ||
| { | ||
| Debug.Assert(property.GetIsNewExtensionMember()); |
There was a problem hiding this comment.
This thread was marked as resolved, but I do not see any related code change and there was no other response.
There was a problem hiding this comment.
The resolution is the added assertion at the top of the method: Debug.Assert(member.GetMemberArityIncludingExtension() != 0);
| throw ExceptionUtilities.UnexpectedValue(member); | ||
| } | ||
|
|
||
| internal static PooledDictionary<TypeParameterSymbol, int>? MakeOrdinalsIfNeeded<TMember>(this TMember member, ImmutableArray<TypeParameterSymbol> originalTypeParameters) |
|
|
||
| // Since we're concatenating type parameters from the extension and from the method together | ||
| // we need to control the ordinals that are used | ||
| ordinals = PooledDictionary<TypeParameterSymbol, int>.GetInstance(); |
There was a problem hiding this comment.
This thread was resolved, but I do not think changing the key to object changes anything about how keys are compared
| // M(object, object) and is therefore the best match. | ||
|
|
||
| return !methodSymbol.CheckConstraints(new ConstraintsHelper.CheckConstraintsArgs(this.Compilation, this.Conversions, includeNullability: false, node.Location, diagnostics)); | ||
| return !methodSymbol.CheckConstraintsIncludingExtension(new ConstraintsHelper.CheckConstraintsArgs(this.Compilation, this.Conversions, includeNullability: false, node.Location, diagnostics)); |
There was a problem hiding this comment.
Will undo this change and the one in In the language versions where we include new extension members in overload resolution, we filter bad candidates (which fail constraint checks) early, so the final validation check doesn't make a difference.GetCandidatesPassingFinalValidation.
But new extensions can make it here, so the helper with assertion cannot be used.
| TMethodOrPropertySymbol member = result.Member; | ||
| if (!MemberGroupFinalValidationAccessibilityChecks(receiverOpt, member, syntax, candidateDiagnostics, invokedAsExtensionMethod: isExtensionMethodGroup && !member.GetIsNewExtensionMember()) && | ||
| (typeArgumentsOpt.IsDefault || ((MethodSymbol)(object)result.Member).CheckConstraints(new ConstraintsHelper.CheckConstraintsArgs(this.Compilation, this.Conversions, includeNullability: false, syntax.Location, candidateDiagnostics)))) | ||
| (typeArgumentsOpt.IsDefault || ((MethodSymbol)(object)result.Member).CheckConstraintsIncludingExtension(new ConstraintsHelper.CheckConstraintsArgs(this.Compilation, this.Conversions, includeNullability: false, syntax.Location, candidateDiagnostics)))) |
| foreach (var m in methods) | ||
| { | ||
| if (!IsUnboundGeneric(m) && m.ParameterCount > 0) | ||
| if (!IsUnboundGeneric(m) && m.GetParametersIncludingExtensionParameter().Length > 0) |
| if (!IsUnboundGeneric(m) && m.ParameterCount > 0) | ||
| if (!IsUnboundGeneric(m) && m.GetParametersIncludingExtensionParameter().Length > 0) | ||
| { | ||
| parameterListList.Add(m.Parameters); |
There was a problem hiding this comment.
Added and tried to observe the change, but I wasn't able to
|
|
||
| MethodSymbol method = (MethodSymbol)(Symbol)result.Member; | ||
| if (!method.CheckConstraints(new ConstraintsHelper.CheckConstraintsArgs(compilation, conversions, includeNullability: false, location, diagnostics))) | ||
| if (!method.CheckConstraintsIncludingExtension(new ConstraintsHelper.CheckConstraintsArgs(compilation, conversions, includeNullability: false, location, diagnostics))) |
There was a problem hiding this comment.
We don't report detailed overload resolution diagnostics for extensions, so I'll undo this.
| { | ||
| // The receiver will be analyzed as an argument instead | ||
| receiver = null; | ||
| return false; |
| AssignmentKind.Assignment); | ||
|
|
||
| bool reportedDiagnostic = enumeratorInfoOpt?.GetEnumeratorInfo.Method is { IsExtensionMethod: true } // Tracked by https://github.com/dotnet/roslyn/issues/76130: Test this code path with new extensions | ||
| bool reportedDiagnostic = enumeratorInfoOpt?.GetEnumeratorInfo.Method is { IsExtensionMethod: true } // Tracked by https://github.com/dotnet/roslyn/issues/76130: Test this code path with new extensions // Tracked by https://github.com/dotnet/roslyn/issues/76130: Test this code path with new extensions |
There was a problem hiding this comment.
| // (4,1): warning CS8602: Dereference of a possibly null reference. | ||
| // s.M(); | ||
| Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(4, 1)); | ||
| comp.VerifyEmitDiagnostics(); |
| #nullable enable | ||
|
|
||
| object? oNull = null; | ||
| oNull.M(); // 1 |
| // Map of TypeParameterSymbol to ordinal for new extension methods | ||
| // When doing type inference on a new extension method, we combine the type parameters | ||
| // from the extension declaration and from the method, so we cannot rely on the ordinals from the type parameters. | ||
| private readonly Dictionary<TypeParameterSymbol, int> _ordinals; |
There was a problem hiding this comment.
In order to get reference equality. We could allocate, we could introduce a new pool, or we could use object as the key type.
| if (!IsUnboundGeneric(m) && m.GetParameterCountIncludingExtensionParameter() > 0) | ||
| { | ||
| parameterListList.Add(m.Parameters); | ||
| parameterListList.Add(m.GetParametersIncludingExtensionParameter()); |
| } | ||
|
|
||
| public static bool RequiresChecking(MethodSymbol method) | ||
| public static bool RequiresCheckingIncludingExtension(MethodSymbol method) |
| { | ||
| method = InferMethodTypeArguments(method, GetArgumentsForMethodTypeInference(results, argumentsNoConversions), refKindsOpt, argsToParamsOpt, expanded); | ||
| parametersOpt = method.Parameters; | ||
| parametersOpt = method.GetParametersIncludingExtensionParameter(); |
There was a problem hiding this comment.
Yes, Nullability_Invocation_09, the object.M2() invocation
| } | ||
|
|
||
| private void CheckMethodConstraints(SyntaxNode syntax, MethodSymbol method) | ||
| private void CheckMethodConstraintsIncludingExtension(SyntaxNode syntax, MethodSymbol method) |
| skipParameters); | ||
| } | ||
|
|
||
| if (method.GetIsNewExtensionMember() && method.ContainingType is { Arity: > 0 } extension |
There was a problem hiding this comment.
if (method.GetIsNewExtensionMember() && method.ContainingType is { Arity: > 0 } extension
As we discussed offline, I think this logic should be moved into the method called above, and quite possibly into much lower level helper, the "root". Therefore, I do not expect implementation of the enclosing method to change. #Closed
|
Done with review pass (commit 4) |
AlekseyTs
left a comment
There was a problem hiding this comment.
LGTM (commit 6), assuming CI is passing
|
@jjonescz for another review. Thanks |
| return; | ||
| } | ||
|
|
||
| ImmutableArray<ParameterSymbol> parameters = method.IsStatic ? method.Parameters : method.GetParametersIncludingExtensionParameter(); |
There was a problem hiding this comment.
method.IsStatic ? method.Parameters : method.GetParametersIncludingExtensionParameter()
It feels like there could be a helper for this common pattern. Even better, the existing helper could have a non-optional parameter specifying what should happen in the static case so callers don't forget to handle that properly. #Resolved
| if (node.ReceiverOpt is not null) | ||
| { | ||
| // Tracked by https://github.com/dotnet/roslyn/issues/76130: Do we need to do anything special for new extensions here? | ||
| VisitRvalueEpilogue(receiver); // VisitRvalue does this after visiting each node |
There was a problem hiding this comment.
Why don't we need to do anything special for new extensions here? Consider adding a comment. #Resolved
There was a problem hiding this comment.
Good catch. I forgot to revisit this when enabling the stack-based optimization.
We need to set extensionReceiverResult properly as it is used as firstArgumentResult: argument in the next iteration of the loop. That way, when we get to visit arguments in reinferMethodAndVisitArguments (and ultimately in VisitArgument) we skip visiting that argument, so that we don't end up visiting it twice.
The added tests verify this.
On the flip side, I couldn't come up with any test case where extensionReceiverResult = _visitResult; wouldn't be good enough. The call to VisitArgumentEvaluateEpilogue is right as far as I can tell, but I was not able to observe it (and neither do existing nullable tests).
There was a problem hiding this comment.
I pushed one more commit for a test that covers this properly. It took me some time to come up with a test but it's possible :-)
|
|
||
| if (argumentRefKindsOpt.IsDefault) | ||
| { | ||
| if (extensionParameter.RefKind == RefKind.None) |
There was a problem hiding this comment.
Shouldn't this check receiverRefKind? Otherwise if extensionParameter.RefKind is In for example, we construct the array unnecessarily. #Resolved
|
|
||
| ParameterSymbol? extensionParameter = method.ContainingType.ExtensionParameter; | ||
| Debug.Assert(extensionParameter is not null); | ||
| var receiverRefKind = extensionParameter.RefKind == RefKind.Ref ? RefKind.Ref : RefKind.None; |
There was a problem hiding this comment.
I think this came up previously, can you remind me why we check only Ref and not other ref kinds? #Resolved
There was a problem hiding this comment.
This follows what we do for classic extension methods. See OverloadReolution.IsApplicable:
if (forExtensionMethodThisArg)
{
Debug.Assert(argumentRefKind == RefKind.None);
if (parameterRefKind == RefKind.Ref)
{
// For ref extension methods, we omit the "ref" modifier on the receiver arguments
// Passing the parameter RefKind for finding the correct conversion.
// For ref-readonly extension methods, argumentRefKind is always None.
argumentRefKind = parameterRefKind;
}
}
So when we need to do inference or argument conversions, we add a ref if the parameter is ref, and we leave none for ref readonly/in. out is a declaration error.
| Debug.Assert(receiverSlot >= 0); | ||
| if (method.GetIsNewExtensionMember()) | ||
| { | ||
| return; |
There was a problem hiding this comment.
Why are post conditions skipped for new extension members? #Resolved
There was a problem hiding this comment.
The are not just "post-conditions", they are "member post-conditions". The attributes that are processed below are MemberNotNull and MemberNotNullWhen.
Normally those apply to members of the containing type, which holds states. My thinking was that extensions don't hold state, so those attributes don't apply.
But extension members could be stateful, so it may make sense after all.
I'll add a follow-up comment to confirm whether we want this to work. If so, we'll need to spec it first. Thanks!
| #nullable enable | ||
|
|
||
| object? oNull = null; | ||
| oNull.M(); // 1 |
There was a problem hiding this comment.
Consider testing the suppression operator (!) as well for at least a few cases. #Resolved
Included:
Out-of-scope:
For attributes, after some discussion, I updated the PR to maximally support the nullability attributes for compatibility/portability.
Filled #78022 (re-inferred method group not properly shown in semantic model and
var)Fixes #78182
Relates to test plan #76130