Skip to content

Extensions: AssociatedExtensionImplementation on constructed symbols#80423

Merged
jcouv merged 4 commits intodotnet:mainfrom
jcouv:extensions-associated
Sep 25, 2025
Merged

Extensions: AssociatedExtensionImplementation on constructed symbols#80423
jcouv merged 4 commits intodotnet:mainfrom
jcouv:extensions-associated

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Sep 23, 2025

Addresses part of #78957 ("AssociatedExtensionImplementation should work on constructed symbols (currently only definitions)")
Closes #80167
Relates to test plan #76130

@jcouv jcouv self-assigned this Sep 23, 2025
@jcouv jcouv added Area-Compilers Feature - Extension Everything The extension everything feature labels Sep 23, 2025
@jcouv jcouv mentioned this pull request Sep 13, 2025
14 tasks
@jcouv jcouv marked this pull request as ready for review September 23, 2025 15:30
@jcouv jcouv requested a review from a team as a code owner September 23, 2025 15:30

if (_underlying.IsDefinition)
{
return implDefinition.GetPublicSymbol();
Copy link
Contributor

Choose a reason for hiding this comment

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

return implDefinition.GetPublicSymbol();

The question is - should we be always constructing generic implementation?

@jcouv jcouv marked this pull request as draft September 23, 2025 21:58
if (extensionMember is IMethodSymbol { OriginalDefinition.AssociatedExtensionImplementation: { } toShadow })
{
implementationsToHide.Add(toShadow);
implementationsToHide.Add(toShadow.OriginalDefinition);
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 previous behavior of the API was to associate a definition with a definition, but now it associates a constructed symbol with a definition.
I'm not sure that's desirable in terms of API design. Maybe it would be better to keep the restriction that the API only works on definitions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Given that I didn't hear back from API review on this question, I'm assuming we're proceeding with design as-is. I re-opened the PR for review. @jjonescz Please take a look

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 3)

@jcouv jcouv marked this pull request as ready for review September 25, 2025 07:29
@jcouv jcouv requested a review from a team as a code owner September 25, 2025 07:29
var memberAccess = GetSyntax<MemberAccessExpressionSyntax>(tree, "t.M");
var method = (IMethodSymbol)model.GetSymbolInfo(memberAccess).Symbol;
Assert.True(method.IsDefinition);
var associated = method.AssociatedExtensionImplementation;
Copy link
Member

Choose a reason for hiding this comment

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

If we have a definition, why couldn't its AssociatedExtensionImplementation return definition too? The API review says it should be consistent with AssociatedSymbol and isn't that what it would do?

Copy link
Member Author

@jcouv jcouv Sep 25, 2025

Choose a reason for hiding this comment

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

Unfortunately, the analogy with AssociatedSymbol breaks down because the situation is different enough.
For AssociatedSymbol, if we have an accessor in a generic type, the associated symbol (ie. the property) will be in the same generic type, with the same substitution. So there's not a situation where the symbols define different type parameters. We can associate C<T>.get_P() with C<T>.P, or C<int>.get_P() with C<int>.P without any issue.

But for generic extension members, the two symbols differ in type parameters.
If the extension method is E.extension<T>(T t).M(), the implementation method is E.M<T'>(T' t) (the prime is added to highlight that they are different type parameters).
If the behavior of the API were to associate E.M<T'>(T' t) (definition) with E.extension<T>(T t).M() (definition), that would be irregular. It is unclear how we would justify it (was it some renaming, some substitution, ...)?

Considering more than one type parameter, what would be expected if you had a partial substitution like E2.M<T1', int>(). Would it associate with E2.extension<T1, int>().M() or E2.extension<T1', int>().M()?

Added you to the thread with API review

Copy link
Member

Choose a reason for hiding this comment

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

If the behavior of the API were to associate E.M<T'>(T' t) (definition) with E.extension<T>(T t).M() (definition), that would be irregular.

I see, makes sense, thanks. (I was already in the API review email thread, but this reasoning wasn't clear from the email to me.)

Considering more than one type parameter, what would be expected if you had a partial substitution like E2.M<T1', int>(). Would it associate with E2.extension<T1, int>().M() or E2.extension<T1', int>().M()?

Do we have a test for that?

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 we have a test for that?

I'll add one in next PR. I'd like to get this in and get it considered for backport to 18.0 since affects behavior of public API

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 do have a test for partial construction. I'll strengthen it to make it more apparent what type parameter is used.

    [Fact]
    public void AssociatedExtensionImplementation_08()
    {
        // not a definition, generic extension and method, partially constructed with type parameters
        var src = """
public static class E
{
    extension<T>(T t)
    {
        public void M<U, V>(U u, V v)
        {
            t.M(42L, "");
        }
    }
}
""";
        var comp = CreateCompilation(src);
        comp.VerifyEmitDiagnostics();

        var tree = comp.SyntaxTrees.First();
        var model = comp.GetSemanticModel(tree);
        var memberAccess = GetSyntax<MemberAccessExpressionSyntax>(tree, "t.M");
        var method = (IMethodSymbol)model.GetSymbolInfo(memberAccess).Symbol;
        AssertEx.Equal("void E.M<T, System.Int64, System.String>(this T t, System.Int64 u, System.String v)",
            method.AssociatedExtensionImplementation.ToTestDisplayString());
    }

@jcouv jcouv merged commit cdc5fd7 into dotnet:main Sep 25, 2025
28 checks passed
@jcouv jcouv deleted the extensions-associated branch September 25, 2025 12:16
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Sep 25, 2025
@jcouv
Copy link
Member Author

jcouv commented Sep 25, 2025

/backport to release/dev18.0

@github-actions
Copy link
Contributor

Started backporting to release/dev18.0: https://github.com/dotnet/roslyn/actions/runs/18007170510

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extensions: AssociatedExtensionImplementation API proposal

4 participants