Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API#80170
Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API#80170jcouv merged 7 commits intodotnet:mainfrom
Conversation
TryGetCorrespondingExtensionImplementationMethod API
66fbb77 to
9ec4c31
Compare
| } | ||
| } | ||
| } | ||
| """); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
| implementationsToHide ??= PooledHashSet<IMethodSymbol>.GetInstance(); | |
| ``` #Resolved |
| return members; | ||
| } | ||
|
|
||
| var result = ArrayBuilder<ISymbol>.GetInstance(); |
There was a problem hiding this comment.
| var result = ArrayBuilder<ISymbol>.GetInstance(); | |
| using var _2 = ArrayBuilder<ISymbol>.GetInstance(out var result); | |
| ``` #Resolved |
| implementationsToHide.Free(); | ||
| return result.ToImmutableAndFree(); |
There was a problem hiding this comment.
| 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) |
There was a problem hiding this comment.
| static IEnumerable<ISymbol> getMembers(INamedTypeSymbol type) | |
| static IEnumerable<ISymbol> GetMembers(INamedTypeSymbol type) | |
| ``` #Resolved |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
do you need to remove? or just check containment? #Resolved
| #nullable enable | ||
| IMethodSymbol? IMethodSymbol.TryGetCorrespondingExtensionImplementationMethod() | ||
| { | ||
| if (!_underlying.IsDefinition || !_underlying.GetIsNewExtensionMember()) |
There was a problem hiding this comment.
curious why the need for the .IsDefinition check. Thanks! #Resolved
There was a problem hiding this comment.
See assertion in NamedTypeSymbol.TryGetCorrespondingExtensionImplementationMethod that we use below
| { | ||
| } | ||
| } | ||
| """); |
There was a problem hiding this comment.
i'd prefer if you kept this consistent with the test above (for indentation) :)
#Resolved
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Link for API review in OP. I'm hoping we'll discuss tomorrow.
I'm sure we'll discuss naming options. Maybe AssociatedExtensionImplementation
There was a problem hiding this comment.
SGTM :) I'll ignre the naming for this review then.
| } | ||
|
|
||
| if (IsKeyword(token.Kind())) | ||
| if (IsKeyword(token.Kind()) && !token.IsKind(SyntaxKind.ExtensionKeyword)) |
There was a problem hiding this comment.
Consider moving the IsKind check to the if below (which has all the other IsKind checks) for consistency #ByDesign
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
| /// For exampe, considering: | |
| /// For example, considering: | |
| ``` #Resolved |
| /// Returns null otherwise. | ||
| /// | ||
| /// For exampe, considering: | ||
| /// ``` |
There was a problem hiding this comment.
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()]; |
There was a problem hiding this comment.
nit:
| 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) |
There was a problem hiding this comment.
Are generic constraints going to work? #Resolved
|
|
||
| throw new ArgumentException("Symbol cannot be converted to a declaration"); | ||
|
|
||
| static IEnumerable<ISymbol> GetMembersMinusExtensionImplementations(INamedTypeSymbol type) |
There was a problem hiding this comment.
nit: "except" seems like a better term for this than "minus" #Resolved
|
|
| { | ||
| foreach (var extensionMember in nested.GetMembers()) | ||
| { | ||
| if (extensionMember is IMethodSymbol { OriginalDefinition.AssociatedExtensionImplementation: { } toShadow }) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
|
||
| return SyntaxFactory.ExtensionBlockDeclaration(attributeLists: default, modifiers: default, ExtensionKeyword, | ||
| typeParameterList, parameterList: AsParameterList([extensionParameter]), | ||
| constraintClauses: default, OpenBraceToken, extensionMembers, CloseBraceToken, default); |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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 :)
|
@CyrusNajmabadi Any additional comments? Let me know if you are also covering the second compiler review, or only the IDE portion. |
|
Looking! |
| { | ||
| } | ||
| } | ||
| """); |
There was a problem hiding this comment.
i might add a test with a static extension method.
There was a problem hiding this comment.
Sorry, I'd missed this feedback. Addressed in #80249
| if (member is IMethodSymbol method) | ||
| { | ||
| // Hide implementation methods | ||
| if (!implementationsToHide.Contains(method.OriginalDefinition)) | ||
| { | ||
| result.Add(member); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| result.Add(member); | ||
| } |
There was a problem hiding this comment.
| 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); | |
| } |
CyrusNajmabadi
left a comment
There was a problem hiding this comment.
Signing off on both IDE and compiler side.
* 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 ...
* 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 ...
* 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 ...
* 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 ...
Public API proposal: #80167
Addresses parts of public API follow-ups for extensions: #78957
Relates to test plan #76130