Skip to content

Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API#80170

Merged
jcouv merged 7 commits intodotnet:mainfrom
jcouv:extensions-generator
Sep 11, 2025
Merged

Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API#80170
jcouv merged 7 commits intodotnet:mainfrom
jcouv:extensions-generator

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Sep 8, 2025

Public API proposal: #80167
Addresses parts of public API follow-ups for extensions: #78957
Relates to test plan #76130

@jcouv jcouv self-assigned this Sep 8, 2025
@jcouv jcouv added Area-Compilers Feature - Extension Everything The extension everything feature labels Sep 8, 2025
@dotnet-policy-service dotnet-policy-service bot added VSCode Needs API Review Needs to be reviewed by the API review council labels Sep 8, 2025
@jcouv jcouv added the Area-IDE label Sep 8, 2025
TryGetCorrespondingExtensionImplementationMethod API
@jcouv jcouv force-pushed the extensions-generator branch from 66fbb77 to 9ec4c31 Compare September 8, 2025 08:24
}
}
}
""");
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

indent. we try not to do raw strings this way in the IDE. #Resolved

static IEnumerable<ISymbol> getMembers(INamedTypeSymbol type)
{
var members = type.GetMembers();
PooledHashSet<IMethodSymbol>? implementationsToHide = null;
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

no need to make this null to begin with. just do:

using var _ = PooledHashSet<IMethodSymbol>.GetInstance(out var implementationsToHide); #Resolved

if (extensionMember is IMethodSymbol shadows &&
shadows.OriginalDefinition.TryGetCorrespondingExtensionImplementationMethod() is { } toShadow)
{
implementationsToHide ??= PooledHashSet<IMethodSymbol>.GetInstance();
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

Suggested change
implementationsToHide ??= PooledHashSet<IMethodSymbol>.GetInstance();
``` #Resolved

return members;
}

var result = ArrayBuilder<ISymbol>.GetInstance();
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

Suggested change
var result = ArrayBuilder<ISymbol>.GetInstance();
using var _2 = ArrayBuilder<ISymbol>.GetInstance(out var result);
``` #Resolved

Comment on lines +865 to +866
implementationsToHide.Free();
return result.ToImmutableAndFree();
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

Suggested change
implementationsToHide.Free();
return result.ToImmutableAndFree();
return result.ToImmutableAndClear();
``` #Resolved


throw new ArgumentException("Symbol cannot be converted to a declaration");

static IEnumerable<ISymbol> getMembers(INamedTypeSymbol type)
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

Suggested change
static IEnumerable<ISymbol> getMembers(INamedTypeSymbol type)
static IEnumerable<ISymbol> GetMembers(INamedTypeSymbol type)
``` #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

IDE uses camelCase to indicate an allocated delegate. and PascalCase to indicate a normal called function/method.

if (member is IMethodSymbol method)
{
// Hide implementation methods
if (!implementationsToHide.Remove(method.OriginalDefinition))
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

do you need to remove? or just check containment? #Resolved

@jcouv jcouv mentioned this pull request Sep 8, 2025
14 tasks
@jcouv jcouv requested a review from CyrusNajmabadi September 8, 2025 09:35
@jcouv jcouv marked this pull request as ready for review September 8, 2025 09:35
@jcouv jcouv requested review from a team as code owners September 8, 2025 09:35
#nullable enable
IMethodSymbol? IMethodSymbol.TryGetCorrespondingExtensionImplementationMethod()
{
if (!_underlying.IsDefinition || !_underlying.GetIsNewExtensionMember())
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

curious why the need for the .IsDefinition check. Thanks! #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.

See assertion in NamedTypeSymbol.TryGetCorrespondingExtensionImplementationMethod that we use below

{
}
}
""");
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

i'd prefer if you kept this consistent with the test above (for indentation) :)

#Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

consider adding test without generic type parameters on the extension as well.

bool IsIterator { get; }

/// <summary>
/// For a method definition in an extension block, returns the corresponding implementation method if one exists.
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

consider an example. i'm somewhat familiar with this space. but even so, knowing what the 'impl method' is vs the extension def isn't too clear to me. #Resolved

/// For a method definition in an extension block, returns the corresponding implementation method if one exists.
/// Returns null otherwise.
/// </summary>
IMethodSymbol? TryGetCorrespondingExtensionImplementationMethod();
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2025

Choose a reason for hiding this comment

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

nit: i presume this went through API review. i do find the name somewhat strange. we don't really use 'Try' in the names of our ISymbol methods (afaict). We sometimes use 'Find' like FindImplementationForInterfaceMember. or we just have things like IMethodSymbol? OverriddenMethod.

So, perhaps this should just be CorrespondingExtensionImplementationMethod? or FindCorrespondingExtensionImplementationMethod? #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.

Link for API review in OP. I'm hoping we'll discuss tomorrow.
I'm sure we'll discuss naming options. Maybe AssociatedExtensionImplementation

Copy link
Contributor

Choose a reason for hiding this comment

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

SGTM :) I'll ignre the naming for this review then.

@jcouv jcouv changed the title Extensions: add SyntaxGenerator support and TryGetCorrespondingExtensionImplementationMethod API Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API Sep 8, 2025
@jcouv jcouv requested review from AlekseyTs and jjonescz September 8, 2025 20:24
}

if (IsKeyword(token.Kind()))
if (IsKeyword(token.Kind()) && !token.IsKind(SyntaxKind.ExtensionKeyword))
Copy link
Member

@jjonescz jjonescz Sep 9, 2025

Choose a reason for hiding this comment

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

Consider moving the IsKind check to the if below (which has all the other IsKind checks) for consistency #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 see there is a check on token embedded in logic below, but I prefer to keep the checks on token here and the checks on next below. I find that rough structure helpful

var model = comp.GetSemanticModel(tree);
var memberAccess = GetSyntax<MemberAccessExpressionSyntax>(tree, "42.M");
var method = (IMethodSymbol)model.GetSymbolInfo(memberAccess).Symbol;
Assert.Null(method.AssociatedExtensionImplementation);
Copy link
Member

@jjonescz jjonescz Sep 9, 2025

Choose a reason for hiding this comment

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

Consider verifying method.OriginalDefinition.AssociatedExtensionImplementation. #Resolved

/// For a method definition in an extension block, returns the corresponding implementation method if one exists.
/// Returns null otherwise.
///
/// For exampe, considering:
Copy link
Member

@jjonescz jjonescz Sep 9, 2025

Choose a reason for hiding this comment

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

Suggested change
/// For exampe, considering:
/// For example, considering:
``` #Resolved

/// Returns null otherwise.
///
/// For exampe, considering:
/// ```
Copy link
Member

@jjonescz jjonescz Sep 9, 2025

Choose a reason for hiding this comment

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

In doc comments, consider using <code> and <c> instead of markdown syntax. #Resolved

IEnumerable<SyntaxNode>? typeParameters,
IEnumerable<SyntaxNode> members)
{
SyntaxList<MemberDeclarationSyntax> extensionMembers = [.. members.Select(m => m as MemberDeclarationSyntax).WhereNotNull()];
Copy link
Member

@jjonescz jjonescz Sep 9, 2025

Choose a reason for hiding this comment

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

nit:

Suggested change
SyntaxList<MemberDeclarationSyntax> extensionMembers = [.. members.Select(m => m as MemberDeclarationSyntax).WhereNotNull()];
SyntaxList<MemberDeclarationSyntax> extensionMembers = [.. members.OfType<MemberDeclarationSyntax>().WhereNotNull()];
``` #Resolved

var compilation = Compile("""
public static class E
{
extension<T>(int)
Copy link
Member

@jjonescz jjonescz Sep 9, 2025

Choose a reason for hiding this comment

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

Are generic constraints going to work? #Resolved


throw new ArgumentException("Symbol cannot be converted to a declaration");

static IEnumerable<ISymbol> GetMembersMinusExtensionImplementations(INamedTypeSymbol type)
Copy link
Member

@jjonescz jjonescz Sep 9, 2025

Choose a reason for hiding this comment

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

nit: "except" seems like a better term for this than "minus" #Resolved

@jcouv jcouv marked this pull request as draft September 9, 2025 17:18
@jcouv
Copy link
Member Author

jcouv commented Sep 9, 2025

Marked as draft as API review decided to move an number of APIs into INamedTypeSymbol. I'll do that as part of #80164

{
foreach (var extensionMember in nested.GetMembers())
{
if (extensionMember is IMethodSymbol { OriginalDefinition.AssociatedExtensionImplementation: { } toShadow })
Copy link
Member

@jjonescz jjonescz Sep 9, 2025

Choose a reason for hiding this comment

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

In API review, you said that the SyntaxGenerator cannot use IsImplicitlyDeclared because the SyntaxGenerator can sometimes emit implicitly declared symbols for ApiDiff. Can you provide an example of that? It seems to me that ApiDiff should always work just with the user-declared APIs since the implicitly declared symbols are fully determined by what the user declares. #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.

What I said is that SourceGenerator needs to work with both source and metadata symbols. But the strategy of using IsImplicitlyDeclared to filter out implementation methods would not work in metadata scenario.

I didn't investigate in details how GenAPI/ApiDiff deals with hiding certain members. I'm not sure if it needs to.
But I did observe that it has custom logic on top of SourceGenerator: https://github.com/dotnet/sdk/blob/main/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs

Regardless, I think from the SourceGenerator perspective, hiding the implementation methods seems the right thing to do and it needs to deal with metadata symbols.

@jcouv jcouv marked this pull request as ready for review September 11, 2025 13:29

return SyntaxFactory.ExtensionBlockDeclaration(attributeLists: default, modifiers: default, ExtensionKeyword,
typeParameterList, parameterList: AsParameterList([extensionParameter]),
constraintClauses: default, OpenBraceToken, extensionMembers, CloseBraceToken, default);
Copy link
Member

@jjonescz jjonescz Sep 11, 2025

Choose a reason for hiding this comment

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

constraintClauses: default

We don't need to be passing any constraint clauses here? #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.

No, we first generate syntax without constraints and then add them.
See ClassDeclaration factory method for another example illustrating how we don't initially pass constraints.
Then you can see how WithTypeParametersAndConstraints is used to amend the generated syntax
I'm not sure why things were structured this way to start with...

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it was because we had a circularity. to have to generate contraints, we needed tyep parameters. but to have type parameters, we needed the symbol. in a fully immutable model, this was challenging. So, instead, we do it two phase. create things like the method/type with the type parameters. then add constreaints.

but this is lost to the annals of time for me now :)

@jcouv
Copy link
Member Author

jcouv commented Sep 11, 2025

@CyrusNajmabadi Any additional comments? Let me know if you are also covering the second compiler review, or only the IDE portion.

@CyrusNajmabadi
Copy link
Contributor

Looking!

{
}
}
""");
Copy link
Contributor

Choose a reason for hiding this comment

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

i might add a test with a static extension 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.

Sorry, I'd missed this feedback. Addressed in #80249

Comment on lines +849 to +860
if (member is IMethodSymbol method)
{
// Hide implementation methods
if (!implementationsToHide.Contains(method.OriginalDefinition))
{
result.Add(member);
}
}
else
{
result.Add(member);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (member is IMethodSymbol method)
{
// Hide implementation methods
if (!implementationsToHide.Contains(method.OriginalDefinition))
{
result.Add(member);
}
}
else
{
result.Add(member);
}
// Hide implementation methods
if (member is not IMethodSymbol method ||
!implementationsToHide.Contains(method.OriginalDefinition))
{
result.Add(member);
}

Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

Signing off on both IDE and compiler side.

@jcouv jcouv merged commit ea9d5bf into dotnet:main Sep 11, 2025
28 checks passed
@jcouv jcouv deleted the extensions-generator branch September 11, 2025 17:36
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Sep 11, 2025
333fred added a commit to 333fred/roslyn that referenced this pull request Sep 11, 2025
* upstream/main: (233 commits)
  Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API (dotnet#80170)
  Fix error when hoisting a non-ref call (dotnet#80138)
  Ensure that refkinds are rewritten for complex methods (dotnet#79916)
  Revert
  Do not go through the workspace to access services
  DefiniteAssignmentPass.MarkFieldsUsed - avoid infinite recursion due to generic substitution (dotnet#80135)
  Reduce allocations in AnalyzerDriver.TryExecuteSymbolEndActions (dotnet#79855)
  RefSafetyAnalysis: Fix handling of nested deconstruction utilizing modern extensions (dotnet#80231)
  Extensions: adjust rewriting of anonymous type property symbols (dotnet#80211)
  Extensions: Move public APIs into INamedTypeSymbol (dotnet#80230)
  Extensions: improve error recovery in older language versions (dotnet#80206)
  Fall back to `dotnet exec` if apphost does not exist (dotnet#80153)
  Update dependencies from https://github.com/dotnet/dotnet build 282708 (dotnet#80228)
  Add a workaround for microsoft/vs-mef#620
  Revert "FailFast if the MEF composition is clearly broken"
  switch from windows combobox to visualstudio combobox (dotnet#80219)
  Update System.Text.Json in packages which use 4.12 Roslyn (dotnet#80197)
  add flags to unblock CI (dotnet#80222)
  Move static members to another type - qualifies static member references in the moved members (dotnet#80178)
  Fix broken link for C# 14 lambda parameter modifiers
  ...
333fred added a commit that referenced this pull request Sep 12, 2025
* upstream/main: (201 commits)
  Handle extension blocks in CLS compliance checker (#80251)
  Fix
  Simplify
  Simplify
  Simplify
  Simplify
  Simplify
  Add doc for GetInferredNullableAnnotation (#80245)
  Rename files to match type within
  Implement ref local hoisting in runtime async. Closes #79763.
  Pull ref initialization hoisting into a reusable class for use in runtime async
  REvert
  Break cycle for lambda using field keyword with inferred return type (#79995)
  Simplify tests
  Simplify
  Fixup
  New extension
  Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API (#80170)
  In process
  Support 'add using' for modern extension methods
  ...
333fred added a commit to 333fred/roslyn that referenced this pull request Sep 12, 2025
* upstream/main: (1725 commits)
  Handle extension blocks in CLS compliance checker (dotnet#80251)
  Fix
  Simplify
  Simplify
  Simplify
  Simplify
  Simplify
  Add doc for GetInferredNullableAnnotation (dotnet#80245)
  Rename files to match type within
  Implement ref local hoisting in runtime async. Closes dotnet#79763.
  Pull ref initialization hoisting into a reusable class for use in runtime async
  REvert
  Break cycle for lambda using field keyword with inferred return type (dotnet#79995)
  Simplify tests
  Simplify
  Fixup
  New extension
  Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API (dotnet#80170)
  In process
  Support 'add using' for modern extension methods
  ...
333fred added a commit to 333fred/roslyn that referenced this pull request Sep 12, 2025
* upstream/main: (131 commits)
  Handle extension blocks in CLS compliance checker (dotnet#80251)
  Fix
  Simplify
  Simplify
  Simplify
  Simplify
  Simplify
  Add doc for GetInferredNullableAnnotation (dotnet#80245)
  Rename files to match type within
  Implement ref local hoisting in runtime async. Closes dotnet#79763.
  Pull ref initialization hoisting into a reusable class for use in runtime async
  REvert
  Break cycle for lambda using field keyword with inferred return type (dotnet#79995)
  Simplify tests
  Simplify
  Fixup
  New extension
  Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API (dotnet#80170)
  In process
  Support 'add using' for modern extension methods
  ...
@akhera99 akhera99 modified the milestones: Next, 18.0 P1, 18.0 P2 Sep 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Area-IDE Feature - Extension Everything The extension everything feature Needs API Review Needs to be reviewed by the API review council VSCode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants