Skip to content

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Apr 1, 2025

  • check extension parameter (like this parameter in classic extension method)
  • checks for parameters of async and iterator methods include the extension parameter
  • init disallowed
  • certain modifiers disallowed
  • instance members and parameter modifiers disallowed if the receiver parameter is unnamed
  • reserve "extension"

Corresponding spec update: dotnet/csharplang#9274

Relates to test plan #76130

@jcouv jcouv added Area-Compilers Feature - Extension Everything The extension everything feature labels Apr 1, 2025
@jcouv jcouv self-assigned this Apr 1, 2025
@ghost ghost added the untriaged Issues and PRs which have not yet been triaged by a lead label Apr 1, 2025
@jcouv jcouv force-pushed the extensions-validation branch from a180d42 to b981f62 Compare April 1, 2025 13:19
@jcouv jcouv force-pushed the extensions-validation branch from b981f62 to 04e23e9 Compare April 1, 2025 13:40
@jcouv jcouv force-pushed the extensions-validation branch from 04e23e9 to 9fe5bae Compare April 1, 2025 15:10
@jcouv jcouv marked this pull request as ready for review April 1, 2025 15:31
@jcouv jcouv requested review from a team as code owners April 1, 2025 15:31
@jcouv jcouv requested review from AlekseyTs and jjonescz April 1, 2025 15:31
internal static bool IsExtensionParameterImplementation(this ParameterSymbol parameter)
{
return parameter.ContainingSymbol is SourceExtensionImplementationMethodSymbol implementationMethod
&& parameter.Ordinal == 0;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

parameter.Ordinal == 0

I think this condition is insufficient and will lead to an incorrect result for static extension methods #Closed

{
diagnostics.Add(ErrorCode.ERR_BadIteratorArgType, parameter.GetFirstLocation());
var location = isReceiverParameter
? ((MethodDeclarationSyntax)iterator.GetNonNullSyntaxNode()).Identifier.GetLocation()
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

((MethodDeclarationSyntax)iterator.GetNonNullSyntaxNode()).Identifier.GetLocation()

Wouldn't simply requesting location from iterator work? #Closed


hasAnyRefOmittedArgument = false;

bool isNewExtensionMember = member.GetIsNewExtensionMember();
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

bool isNewExtensionMember = member.GetIsNewExtensionMember();

Consider moving declaration closer to usage #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 1, 2025

            if (member.GetIsNewExtensionMember())

isNewExtensionMember? #Closed


Refers to: src/Compilers/CSharp/Portable/Binder/Semantics/OverloadResolution/OverloadResolution.cs:3890 in 9fe5bae. [](commit_id = 9fe5bae, deletion_comment = False)

Debug.Assert((object)symbol != null);
Debug.Assert(arguments != null);

bool isNewExtensionMember = symbol.GetIsNewExtensionMember();
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

bool isNewExtensionMember = symbol.GetIsNewExtensionMember();

It looks like the local is no longer used #Closed


internal static bool IsExtensionParameterImplementation(this ParameterSymbol parameter)
{
return parameter.ContainingSymbol is SourceExtensionImplementationMethodSymbol implementationMethod
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

parameter

It looks like we are making an assumption that this method is always called on a definition. Consider asserting the fact. #Closed

else if (parameterRefKind is RefKind.In or RefKind.RefReadOnlyParameter && parameterType.TypeKind != TypeKind.Struct)
{
diagnostics.Add(ErrorCode.ERR_InExtensionParameterMustBeValueType, parameterTypeSyntax);
}
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

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

Copy link
Member Author

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

Copy link
Member

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))
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

!p.IsExtensionParameterImplementation()

It is not obvious why extension parameter is getting special treatment #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.

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.

Copy link
Contributor

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.

Copy link
Member Author

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)

Copy link
Contributor

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?

Copy link
Member Author

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);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

Name

Will the name work for an indexer? #Closed

get { return _underlyingParameter.RefCustomModifiers; }
}

internal sealed override bool HasEnumeratorCancellationAttribute
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

internal sealed override bool HasEnumeratorCancellationAttribute

I think we should override this property only in leaves #Closed

}

[Fact]
public void Member_InstanceIndexer_Metadata()
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

Member_InstanceIndexer_Metadata

It feels like this test isn't very useful right now. At least in the form that uses IL. I think we can compile an indexer declaration to metadata. #Closed

diagnostics.Add(ErrorCode.ERR_InExtensionParameterMustBeValueType, parameterTypeSyntax);
}

if (parameter.Name is "" && parameterRefKind != RefKind.None)
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

parameterRefKind != RefKind.None

Should scoped be disallowed as well? #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, 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 }; }
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

42

i? #Closed

""";
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
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

warning CS8424: The EnumeratorCancellationAttribute applied to parameter 'token1' will have no effect.

Is this warning misleading and likely incorrect in a general 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.

The warning is correct for the current design/implementation. See detailed explanation in other response

}

[Fact]
public void ReceiverParameterValidation_UnnamedReceiverParameter()
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

ReceiverParameterValidation_UnnamedReceiverParameter

Consider testing indexers as well #Closed

}

[Fact]
public void Validation_Modifiers_Abstract()
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 1, 2025

Choose a reason for hiding this comment

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

Validation_Modifiers_Abstract

Consider testing indexers as well #Closed


internal static bool IsExtensionParameterImplementation(this ParameterSymbol parameter)
{
Debug.Assert(parameter.ContainingSymbol.IsDefinition);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

parameter.ContainingSymbol.IsDefinition

Debug.Assert(parameter.IsDefinition);? #Closed

// (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
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

error CS0751: A partial member must be declared within a partial type

Do we understand why this error is reported twice for methods and only once for properties/indexers? #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 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
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

&& !implementationMethod.UnderlyingMethod.IsStatic

It looks like this fix was not covered by a corresponding test. Was it not possible to observe the incorrect behavior without a direct API test? #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.

Will add a test

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 4)


## `extension` treated as a contextual keyword

PROTOTYPE record which version this break is introduced in
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

PROTOTYPE

Shouldn't one be able to use this word? #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.

Not in the main branch

@jcouv jcouv requested a review from AlekseyTs April 2, 2025 14:28
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 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
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 2, 2025

Choose a reason for hiding this comment

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

warning CS8424: The EnumeratorCancellationAttribute applied to parameter 'token1' will have no effect.

Given the speclet wording "The receiver parameter may not have an [EnumeratorCancellation] attribute." I would expect an error instead. #Resolved

Copy link
Member Author

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

@jcouv
Copy link
Member Author

jcouv commented Apr 2, 2025

@jjonescz for second review. Thanks

@AlekseyTs AlekseyTs added the breaking-change https://github.com/dotnet/sdk/blob/main/documentation/project-docs/breaking-change-guidelines.md label Apr 2, 2025

PROTOTYPE record which version this break is introduced in

***Introduced in Visual Studio 2022 version 17.4.***
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

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.

Suggested change
***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()
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

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

Copy link
Member Author

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);
}
Copy link
Member

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)
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

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

Suggested change
else if (ContainingType is { IsExtension: true, ExtensionParameter: { Name: "" } } && !IsStatic)
else if (ContainingType is { IsExtension: true, ExtensionParameter.Name: "" } && !IsStatic)
``` #Resolved

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

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)
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

Choose a reason for hiding this comment

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

Suggested change
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;
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

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 = $$"""
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

Choose a reason for hiding this comment

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

Suggested change
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 = """
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

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.***
Copy link
Member

@jjonescz jjonescz Apr 3, 2025

Choose a reason for hiding this comment

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

Suggested change
***Introduced in Visual Studio 2022 version 17.4.***
***Introduced in Visual Studio 2022 version 17.14.***
``` #Resolved

@jcouv jcouv enabled auto-merge (squash) April 3, 2025 08:01
@jcouv jcouv merged commit f72da0b into dotnet:main Apr 3, 2025
28 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Apr 3, 2025
@jcouv jcouv deleted the extensions-validation branch April 3, 2025 11:04
@RikkiGibson RikkiGibson modified the milestones: Next, 18.0 P1 Aug 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers breaking-change https://github.com/dotnet/sdk/blob/main/documentation/project-docs/breaking-change-guidelines.md Feature - Extension Everything The extension everything feature untriaged Issues and PRs which have not yet been triaged by a lead VSCode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants