-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Extensions: misc checks on receiver parameter and extension members #77937
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
Conversation
a180d42 to
b981f62
Compare
b981f62 to
04e23e9
Compare
04e23e9 to
9fe5bae
Compare
| internal static bool IsExtensionParameterImplementation(this ParameterSymbol parameter) | ||
| { | ||
| return parameter.ContainingSymbol is SourceExtensionImplementationMethodSymbol implementationMethod | ||
| && parameter.Ordinal == 0; |
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.
| { | ||
| diagnostics.Add(ErrorCode.ERR_BadIteratorArgType, parameter.GetFirstLocation()); | ||
| var location = isReceiverParameter | ||
| ? ((MethodDeclarationSyntax)iterator.GetNonNullSyntaxNode()).Identifier.GetLocation() |
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.
|
|
||
| hasAnyRefOmittedArgument = false; | ||
|
|
||
| bool isNewExtensionMember = member.GetIsNewExtensionMember(); |
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.
| Debug.Assert((object)symbol != null); | ||
| Debug.Assert(arguments != null); | ||
|
|
||
| bool isNewExtensionMember = symbol.GetIsNewExtensionMember(); |
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.
|
|
||
| internal static bool IsExtensionParameterImplementation(this ParameterSymbol parameter) | ||
| { | ||
| return parameter.ContainingSymbol is SourceExtensionImplementationMethodSymbol 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.
| else if (parameterRefKind is RefKind.In or RefKind.RefReadOnlyParameter && parameterType.TypeKind != TypeKind.Struct) | ||
| { | ||
| diagnostics.Add(ErrorCode.ERR_InExtensionParameterMustBeValueType, parameterTypeSyntax); | ||
| } |
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.
I think it would be desirable to try sharing logic with ExtensionMethodChecks in order to avoid an accidental diverging #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.
I agree and I tried that initially, but the logic for these three checks in ExtensionMethodChecks uses locations and also the name of the method
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.
Consider leaving a comment in both places saying that we should keep them in sync.
| _disposeModeField = F.StateMachineField(boolType, GeneratedNames.MakeDisposeModeFieldName()); | ||
|
|
||
| if (_isEnumerable && this.method.Parameters.Any(static p => p.IsSourceParameterWithEnumeratorCancellationAttribute())) | ||
| if (_isEnumerable && this.method.Parameters.Any(static p => !p.IsExtensionParameterImplementation() && p.HasEnumeratorCancellationAttribute)) |
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.
Do you mean we should leave a comment here (the attribute is ignored on extension parameter) or do you mean you think the attribute should be effective on the extension parameter too?
The reason I don't think the attribute should be effective is that this attribute is very specific to async-iterator method implementations. I don't think it belongs on an extension parameter, as the extension block may contain many kinds of members. Also, the scenario where you need an async-iterator extension method on a CancellationToken receiver seems very niche to say the least.
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.
I don't think it belongs on an extension parameter
I am fine with this design decision, Could you please mention this explicitly in the speclet?
Also, please make sure to test scenario where extension parameter implementation is the only parameter for the method.
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.
Sounds good. Linked to corresponding spec update.
The test ReceiverParameterValidation_CancellationTokenParameter_Instance shows that the attribute doesn't have an effect (it includes another parameter, but I think that's fine)
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 includes another parameter, but I think that's fine
Are you saying the CancellationToken parameter doesn't have to be the last parameter in a general case?
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.
That's correct. The only restriction is that only one parameter may be marked with this attribute.
| } | ||
| else if (ContainingType is { IsExtension: true, ExtensionParameter: { Name: "" } } && !IsStatic) | ||
| { | ||
| diagnostics.Add(ErrorCode.ERR_InstanceMemberWithUnnamedExtensionsParameter, location, Name); |
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.
| get { return _underlyingParameter.RefCustomModifiers; } | ||
| } | ||
|
|
||
| internal sealed override bool HasEnumeratorCancellationAttribute |
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.
| } | ||
|
|
||
| [Fact] | ||
| public void Member_InstanceIndexer_Metadata() |
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.
| diagnostics.Add(ErrorCode.ERR_InExtensionParameterMustBeValueType, parameterTypeSyntax); | ||
| } | ||
|
|
||
| if (parameter.Name is "" && parameterRefKind != RefKind.None) |
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, but that's currently a parsing error. See ReceiverParameterValidation_UnnamedReceiverParameter_Scoped, which has a comment for follow-up
| extension(ref MyCollection c) | ||
| { | ||
| public void Add(int i) { System.Console.Write("ran "); c = null; } | ||
| public void Add(int i) { System.Console.Write("ran "); c = new MyCollection() { field = 42 }; } |
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 comp = CreateCompilation(source, targetFramework: TargetFramework.Net90); | ||
| comp.VerifyDiagnostics( | ||
| // (7,16): warning CS8424: The EnumeratorCancellationAttribute applied to parameter 'token1' will have no effect. The attribute is only effective on a parameter of type CancellationToken in an async-iterator method returning IAsyncEnumerable |
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.
The warning is correct for the current design/implementation. See detailed explanation in other response
| } | ||
|
|
||
| [Fact] | ||
| public void ReceiverParameterValidation_UnnamedReceiverParameter() |
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.
| } | ||
|
|
||
| [Fact] | ||
| public void Validation_Modifiers_Abstract() |
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.
|
|
||
| internal static bool IsExtensionParameterImplementation(this ParameterSymbol parameter) | ||
| { | ||
| Debug.Assert(parameter.ContainingSymbol.IsDefinition); |
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.
| // (5,22): error CS0751: A partial member must be declared within a partial type | ||
| // partial void M(); | ||
| Diagnostic(ErrorCode.ERR_PartialMemberOnlyInPartialClass, "M").WithLocation(5, 22), | ||
| // (6,22): error CS0751: A partial member must be declared within a partial type |
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 is an existing behavior. There's a difference between properties and methods in that regard.
| { | ||
| Debug.Assert(parameter.ContainingSymbol.IsDefinition); | ||
| return parameter.ContainingSymbol is SourceExtensionImplementationMethodSymbol implementationMethod | ||
| && !implementationMethod.UnderlyingMethod.IsStatic |
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.
Will add a test
|
Done with review pass (commit 4) |
|
|
||
| ## `extension` treated as a contextual keyword | ||
|
|
||
| PROTOTYPE record which version this break is introduced in |
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.
Not in the main branch
AlekseyTs
left a comment
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 7)
| """; | ||
| var comp = CreateCompilation(source, targetFramework: TargetFramework.Net90); | ||
| comp.VerifyDiagnostics( | ||
| // (7,16): warning CS8424: The EnumeratorCancellationAttribute applied to parameter 'token1' will have no effect. The attribute is only effective on a parameter of type CancellationToken in an async-iterator method returning IAsyncEnumerable |
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.
Thanks. I'll fix the spec
|
@jjonescz for second review. Thanks |
|
|
||
| PROTOTYPE record which version this break is introduced in | ||
|
|
||
| ***Introduced in Visual Studio 2022 version 17.4.*** |
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 change part of this PR? Then it should be probably 17.15. If it was already merged, then 17.14.
| ***Introduced in Visual Studio 2022 version 17.4.*** | |
| ***Introduced in Visual Studio 2022 version 17.14.*** | |
| ``` #Resolved |
|
|
||
| foreach (var parameter in iterator.Parameters) | ||
| var parameters = !iterator.IsStatic | ||
| ? iterator.GetParametersIncludingExtensionParameter() |
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 the GetParametersIncludingExtensionParameter helper check that the member is not static? #Resolved
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.
That would not be convenient. That method is also used in overload resolution, and for the purpose of overload resolution, we do want to account for the extension parameter even when the method is static (the static receiver counts as an argument in that case).
| else if (parameterRefKind is RefKind.In or RefKind.RefReadOnlyParameter && parameterType.TypeKind != TypeKind.Struct) | ||
| { | ||
| diagnostics.Add(ErrorCode.ERR_InExtensionParameterMustBeValueType, parameterTypeSyntax); | ||
| } |
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.
Consider leaving a comment in both places saying that we should keep them in sync.
| { | ||
| diagnostics.Add(AccessCheck.GetProtectedMemberInSealedTypeError(ContainingType), location, this); | ||
| } | ||
| else if (ContainingType is { IsExtension: true, ExtensionParameter: { Name: "" } } && !IsStatic) |
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.
nit: this can be simplified
| else if (ContainingType is { IsExtension: true, ExtensionParameter: { Name: "" } } && !IsStatic) | |
| else if (ContainingType is { IsExtension: true, ExtensionParameter.Name: "" } && !IsStatic) | |
| ``` #Resolved |
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.
ExtensionParameter can be null even when IsExtension is true. That happens when the extension parameter is __arglist.
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, but the pattern would handle nulls right?
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.
Ah, you're right. I'll update
| { | ||
| diagnostics.Add(AccessCheck.GetProtectedMemberInSealedTypeError(ContainingType), location, this); | ||
| } | ||
| else if (ContainingType is { IsExtension: true, ExtensionParameter: { Name: "" } } && !IsStatic) |
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.
| else if (ContainingType is { IsExtension: true, ExtensionParameter: { Name: "" } } && !IsStatic) | |
| else if (ContainingType is { IsExtension: true, ExtensionParameter.Name: "" } && !IsStatic) | |
| ``` #Resolved |
| internal void ReportAsyncParameterErrors(BindingDiagnosticBag diagnostics, Location location) | ||
| { | ||
| foreach (var parameter in Parameters) | ||
| var parameters = !this.IsStatic ? this.GetParametersIncludingExtensionParameter() : Parameters; |
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 could be also simplified if we checked IsStatic inside GetParametersIncludingExtensionParameter. #ByDesign
| public void ReceiverParameter_RefReadonly() | ||
| public void ReceiverParameter_In_03() | ||
| { | ||
| var src = $$""" |
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 src = $$""" | |
| var src = """ | |
| ``` #Resolved |
| public void InstanceMethodInvocation_Simple_ExpressionTree() | ||
| { | ||
| // Tracked by https://github.com/dotnet/roslyn/issues/76130 : decide whether to allow expression tree scenarios. Verify shape of the tree if we decide to allow | ||
| var source = """ |
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 looks like we are not verifying the shape of the tree as the prototype comment suggested. #Resolved
|
|
||
| PROTOTYPE record which version this break is introduced in | ||
|
|
||
| ***Introduced in Visual Studio 2022 version 17.4.*** |
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.
| ***Introduced in Visual Studio 2022 version 17.4.*** | |
| ***Introduced in Visual Studio 2022 version 17.14.*** | |
| ``` #Resolved |
thisparameter in classic extension method)initdisallowedCorresponding spec update: dotnet/csharplang#9274
Relates to test plan #76130