Skip to content

Extensions: refine tracking of used imports#80485

Merged
jcouv merged 7 commits intodotnet:mainfrom
jcouv:extensions-usings
Oct 2, 2025
Merged

Extensions: refine tracking of used imports#80485
jcouv merged 7 commits intodotnet:mainfrom
jcouv:extensions-usings

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Sep 28, 2025

Previously, all using directives that included extension blocks would have counted the using as "used". Now, the lookup is based on specific name, arity and options we're looking for, and only extension members that match will result in the using counting as "used".
The symbols reported from the semantic model APIs (CandidateSymbols and GetMemberGroup()) are also affected/trimmed.

Addresses part of #79440 ("Consider refining GetExtensionDeclarations logic (shown below) to tracked used usings more finely")

Relates to test plan #76130

@jcouv jcouv self-assigned this Sep 28, 2025
@jcouv jcouv added Area-Compilers Feature - Extension Everything The extension everything feature labels Sep 28, 2025
@jcouv jcouv marked this pull request as ready for review September 29, 2025 15:11
@jcouv jcouv requested a review from a team as a code owner September 29, 2025 15:11
@AlekseyTs
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

/// An alternativeName should only provided if a name is provided.
/// </summary>
internal virtual void GetExtensionDeclarations(ArrayBuilder<NamedTypeSymbol> extensions, Binder originalBinder)
internal virtual void GetCandidateExtensionMembers(
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

Consider adding an "InSingleBinder" suffix to the name #Closed

string nameTrue = OperatorFacts.UnaryOperatorNameFromOperatorKind(UnaryOperatorKind.True, isChecked: false);
signature.Method.OriginalDefinition.ContainingType.ContainingType.GetExtensionMembers(extensionCandidates,
nameTrue, alternativeName: null, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember);
Copy link
Member

@jjonescz jjonescz Sep 30, 2025

Choose a reason for hiding this comment

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

Consider moving this common logic into unaryOperatorOverloadResolution #ByDesign

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 prefer leaving the call to GetCandidateExtensionMembers in the same code where the ArrayBuilder is declared. So we declared, populate, clear and free it in the same place

/// Return the extension members from this specific binding scope
/// Since the lookup of extension members is iterative, proceeding one binding scope at a time,
/// GetExtensionDeclarations should not defer to the next binding scope. Instead, the caller is
/// GetCandiateExtensionMembers should not defer to the next binding scope. Instead, the caller is
Copy link
Member

@jjonescz jjonescz Sep 30, 2025

Choose a reason for hiding this comment

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

Suggested change
/// GetCandiateExtensionMembers should not defer to the next binding scope. Instead, the caller is
/// GetCandidateExtensionMembers should not defer to the next binding scope. Instead, the caller is
``` #Resolved

/// GetCandiateExtensionMembers should not defer to the next binding scope. Instead, the caller is
/// responsible for walking the nested binding scopes from innermost to outermost. This method is overridden
/// to search the available members list in binding types that represent types, namespaces, and usings.
/// An alternativeName should only provided if a name is provided.
Copy link
Member

@jjonescz jjonescz Sep 30, 2025

Choose a reason for hiding this comment

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

Suggested change
/// An alternativeName should only provided if a name is provided.
/// An alternativeName should only be provided if a name is provided.
``` #Resolved

}

#nullable enable
internal static bool IsInvalidExtensionReceiverParameter(ParameterSymbol thisParam)
Copy link
Member

@jjonescz jjonescz Sep 30, 2025

Choose a reason for hiding this comment

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

Can this be private? Also consider inverting the logic and simplifying this to "is valid extension receiver parameter". #Resolved

}

internal void GetExtensionContainers(ArrayBuilder<NamedTypeSymbol> extensions)
internal void GetExtensionMembers(ArrayBuilder<Symbol> members, string? name, string? alternativeName, int arity, LookupOptions options)
Copy link
Member

@jjonescz jjonescz Sep 30, 2025

Choose a reason for hiding this comment

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

Should we assert that only LookupOptions.AllMethodsOnArityZero or LookupOptions.MustBeInvocableIfMember are specified? #Resolved

}
""";

internal string ExtensionMarkerAttributeIL = """
Copy link
Member

@jjonescz jjonescz Sep 30, 2025

Choose a reason for hiding this comment

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

Suggested change
internal string ExtensionMarkerAttributeIL = """
internal static readonly string ExtensionMarkerAttributeIL = """
``` #Resolved

extensionCandidatesInSingleScope.Clear();
scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,
ordinaryInstanceOperatorName, checkedInstanceOperatorName, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember, this);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember

Add MustBeInstance? #Closed

extensionDeclarationsInSingleScope.Clear();
scope.Binder.GetExtensionDeclarations(extensionDeclarationsInSingleScope, this);
extensionCandidatesInSingleScope.Clear();
scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,

It looks like this call belongs inside the following if. #Closed

extensionCandidatesInSingleScope.Clear();
scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,
staticOperatorName1, staticOperatorName2Opt, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember, this);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember

Add MustNotBeInstance? #Closed

}

internal static void AddOperators(ArrayBuilder<MethodSymbol> operators, ImmutableArray<Symbol> candidates)
internal static void AddOperators(ArrayBuilder<MethodSymbol> operators, IEnumerable<Symbol> candidates)
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

IEnumerable

Consider adding an overload for an ArrayBuilder instead #Closed

string nameTrue = OperatorFacts.UnaryOperatorNameFromOperatorKind(UnaryOperatorKind.True, isChecked: false);
signature.Method.OriginalDefinition.ContainingType.ContainingType.GetExtensionMembers(extensionCandidates,
nameTrue, alternativeName: null, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember

Add MustNotBeInstance? #Closed

extensionCandidates.Clear();
signature.Method.OriginalDefinition.ContainingType.ContainingType.GetExtensionMembers(extensionCandidates,
nameFalse, alternativeName: null, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember

Add MustNotBeInstance? #Closed

extensionCandidatesInSingleScope.Clear();
scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,
name1, name2Opt, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember, this);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember

Add MustNotBeInstance? #Closed

extensionCandidatesInSingleScope.Clear();
scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,
name1, name2Opt, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember, this);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember

Add MustNotBeInstance? #Closed

extensionCandidatesInSingleScope.Clear();
scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,
staticOperatorName1, staticOperatorName2Opt, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember, this);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember

Add MustNotBeInstance? #Closed

extensionCandidatesInSingleScope.Clear();
scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,
ordinaryInstanceOperatorName, checkedInstanceOperatorName, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember, this);
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember

Add MustBeInstance? #Closed

extensionDeclarationsInSingleScope.Clear();
scope.Binder.GetExtensionDeclarations(extensionDeclarationsInSingleScope, this);
extensionCandidatesInSingleScope.Clear();
scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

scope.Binder.GetCandidateExtensionMembers(extensionCandidatesInSingleScope,

It looks like this call belongs inside the following if #Closed

return;
}

if (extensionDeclaration.ExtensionParameter is not { } extensionParameter ||
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

if (extensionDeclaration.ExtensionParameter is not { } extensionParameter ||

Where these checks are performed now? #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.

Thanks for catching that. I'll restore the type checks. The check for extension parameter being null is in GetExtensionMembers now

{
Debug.Assert(extensionDeclaration.IsExtension);

if (extensionDeclaration.ExtensionParameter is null)
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

if (extensionDeclaration.ExtensionParameter is null)

Where this check is performed now? #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.

NamedTypeSymbol.GetExtensionMembers(...)

{
Debug.Assert(extensionDeclaration.IsExtension);

if (extensionDeclaration.ExtensionParameter is null)
Copy link
Contributor

@AlekseyTs AlekseyTs Sep 30, 2025

Choose a reason for hiding this comment

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

if (extensionDeclaration.ExtensionParameter is null)

Where this check is performed now? #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.

Same

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Sep 30, 2025

Done with review pass (commit 3), tests aren't reviewed #Closed

@jcouv jcouv requested review from AlekseyTs and jjonescz September 30, 2025 19:15

internal void GetExtensionMembers(ArrayBuilder<Symbol> members, string? name, string? alternativeName, int arity, LookupOptions options)
{
Debug.Assert((options & ~(LookupOptions.IncludeExtensionMembers | LookupOptions.AllMethodsOnArityZero
Copy link
Member

@jjonescz jjonescz Oct 1, 2025

Choose a reason for hiding this comment

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

This method does not seem to actually check LookupOptions.IncludeExtensionMembers #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.

It doesn't look at it, but may receive that option.
It didn't feel necessary to require this options, as many of the callers directly call GetCandidateExtensionMemberInSingleScope so I don't think they should have to further signify that they want extensions.

string nameTrue = OperatorFacts.UnaryOperatorNameFromOperatorKind(UnaryOperatorKind.True, isChecked: false);
signature.Method.OriginalDefinition.ContainingType.ContainingType.GetExtensionMembers(extensionCandidates,
nameTrue, alternativeName: null, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember | LookupOptions.MustNotBeInstance);
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 1, 2025

Choose a reason for hiding this comment

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

MustBeInvocableIfMember

MustBeOperator? #Closed

extensionCandidates.Clear();
signature.Method.OriginalDefinition.ContainingType.ContainingType.GetExtensionMembers(extensionCandidates,
nameFalse, alternativeName: null, arity: 0,
LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInvocableIfMember | LookupOptions.MustNotBeInstance);
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 1, 2025

Choose a reason for hiding this comment

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

MustBeInvocableIfMember

MustBeOperator? #Closed


#nullable disable

using System;
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 1, 2025

Choose a reason for hiding this comment

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

Is the using used? #Closed

}

if ((options & LookupOptions.MustBeInvocableIfMember) != 0
&& !Binder.IsInvocableMember(member, fieldsBeingBound: ConsList<FieldSymbol>.Empty))
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 1, 2025

Choose a reason for hiding this comment

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

fieldsBeingBound: ConsList.Empty

This looks suspicious and fragile. The list should probably be passed in, and, when Binder triggers the call we should pass one from the Binder that represents binding context, not the extension scoped being looked at. #Closed

return true;
}

internal void GetExtensionMembers(ArrayBuilder<Symbol> members, string? name, string? alternativeName, int arity, LookupOptions options)
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 1, 2025

Choose a reason for hiding this comment

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

GetExtensionMembers

It might be worth adding a comment saying that this method doesn't perform complete lookup viability check (like accessibility, etc.) And for other APIs that take LookupOptions with the only purpose to pass it to this method as well. #Closed


foreach (var candidate in candidates)
{
if (extensionMemberMatches(candidate, name, alternativeName, arity, options))
Copy link
Contributor

@AlekseyTs AlekseyTs Oct 1, 2025

Choose a reason for hiding this comment

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

if (extensionMemberMatches(candidate, name, alternativeName, arity, options))

Should we be filtering out types? #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 don't think we need an extra filter. There's no lookup option to exclude types. So it's up to the caller to either request an operator, an invocable member, or deal with various members we could find.

Copy link
Contributor

Choose a reason for hiding this comment

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

Types are not supported in extensions, I suggest to filter them out always, not based on any option

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 1, 2025

Done with review pass (commit 5) #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Oct 1, 2025

Done with review pass (commit 6). It looks like an assert related to the changes in this PR is firing in CI #Closed

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)

@jcouv jcouv merged commit 7d32904 into dotnet:main Oct 2, 2025
24 checks passed
@jcouv jcouv deleted the extensions-usings branch October 2, 2025 13:44
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Oct 2, 2025
@davidwengier davidwengier modified the milestones: Next, 18.3 Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Feature - Extension Everything The extension everything feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants