-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Extensions: allow cref references to extension members #78735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| } | ||
|
|
||
| builder.Append(symbol.IsExtension ? symbol.ExtensionName : symbol.Name); | ||
| builder.Append(symbol.IsExtension ? escape(symbol.ExtensionName) : symbol.Name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 I debated whether to escape here, or push that responsibility to the caller of DocumentationCommentIDVisitor. What do you think? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we replace </> with {/} for type argument list below, it probably makes sense to avoid using them here as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some additional thinking about this issue. Now, I think we need to consider possible usage scenarios for the ids that we produce here. What users actually might want to do with them. On one hand, the ids are included into an XML document and reserved XML characters must be replaced with XML entities inside the document. On the other hand, if I am not mistaken, when an XML document is read by an XML reader, the reader replaces the XML entities back to the corresponding characters. Is there a scenario when one tries to match an id gotten from an XML reader with an id that we produce here and exposing through GetDocumentationCommentId API? Or vise versa? If so, doing this transformation here is not the right thing to do. The replacement for </> with {/} for type argument lists is different, it doesn't involve XML entities, an XML reader wouldn't reverse it.
I am leaning towards doing one of the following:
- Moving this "escaping" to the step that serializes an XML document. Basically, during serialization all ids should be checked for presence of
</>and they should be replaced with corresponding XML entities. - Or we can by pass the problem by simply avoiding using reserved XML characters in unspeakable extension names that we generate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One additional data point: in error scenarios, we already escape CREF strings in ToBadCrefString (that's done outside of GetDocumentationCommentId.
I'll move the escaping outside for now. We can discuss the other option (using a different unspeakable name) in WG.
| <Field Name="DotToken" Type="SyntaxToken"> | ||
| <Kind Name="DotToken"/> | ||
| </Field> | ||
| <Field Name="Member" Type="MemberCrefSyntax"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The member cref syntax currently covered name members, indexers, operators and conversion operators. We'll need all those as extension members at some point.
I think the only questionable scenario then is nested extensions.
If we really care to block this in the syntax model, I think we'd need to create an additional level in the hierarchy:
MemberCrefSyntax <- NonExtensionCrefSyntax <- (existing types for named members, indexers, etc)
Then ExtensionMemberCrefSyntax would only hold a NonExtensionCrefSyntax as its member.
I think such model seems a bit overkill and the flexibility doesn't disturb me. We can just restrict the scenario in the parser...
This is scheduled for discussion in API review tomorrow.
Is there a corresponding proposal for the grammar change?
Not yet. I didn't find a spec baseline for the CREF grammar... The Annex D section is as close as it gets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the following question was not answered:
Could we restrict allowed nodes by kind?
I was specifically asking about an ability to restrict nodes by kind in this description. For example, like we do this for tokens:
<Field Name="ExtensionKeyword" Type="SyntaxToken">
<Kind Name="ExtensionKeyword"/>
</Field>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarification. Adding <Kind Name="NameMemberCrefSyntax"/> to the Member doesn't do anything even after regenerating compiler code. WriteGreenFactory only validates Kind on fields of type SyntaxToken.
I'll block the nested scenario by adding an error in parser.
| } | ||
|
|
||
| SyntaxToken dotToken = EatToken(SyntaxKind.DotToken); | ||
| MemberCrefSyntax member = ParseMemberCref(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was no response to this comment. To clarify, I think we need to restrict the parsing and not accept nested extensions even if our syntax nodes technically allow creating such a hierarchy.
|
|
||
| TypeArgumentListSyntax? extensionTypeArguments = syntax.TypeArgumentList; | ||
| int extensionArity = extensionTypeArguments?.Arguments.Count ?? 0; | ||
| sortedSymbols = computeSortedAndFilteredCrefExtensionMembers(namedContainer, simpleName.Identifier.ValueText, extensionArity, arity, extensionTypeArguments, diagnostics, syntax); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (sortedSymbols.IsDefaultOrEmpty) | ||
| { | ||
| ambiguityWinner = null; | ||
| return []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| refCustomModifiers: [], | ||
| explicitInterfaceImplementations: []); | ||
|
|
||
| ImmutableArray<ParameterSymbol> extensionParameterSymbols = syntax.Parameters is { } extensionParameterListSyntax ? BindCrefParameters(extensionParameterListSyntax, diagnostics) : default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| var constructedNested = (NamedTypeSymbol)ConstructWithCrefTypeParameters(extensionArity, extensionTypeArguments, nested); | ||
|
|
||
| var candidateExtensionSignature = new SignatureOnlyMethodSymbol( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
|
||
| ImmutableArray<ParameterSymbol> extensionParameterSymbols = syntax.Parameters is { } extensionParameterListSyntax ? BindCrefParameters(extensionParameterListSyntax, diagnostics) : default; | ||
|
|
||
| var providedExtensionSignature = new SignatureOnlyMethodSymbol( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need an arity which is specific to an iteration of the loop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need an arity which is specific to an iteration of the loop
Don't we make sure nested.Arity == extensionArity when we reach this point, and extensionArity doesn't change in the loop?
| refCustomModifiers: [], | ||
| explicitInterfaceImplementations: []); | ||
|
|
||
| ImmutableArray<ParameterSymbol> extensionParameterSymbols = syntax.Parameters is { } extensionParameterListSyntax ? BindCrefParameters(extensionParameterListSyntax, diagnostics) : default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| case SyntaxKind.ConversionOperatorMemberCref: | ||
| return ((ConversionOperatorMemberCrefSyntax)crefSyntax).Parameters != null; | ||
| case SyntaxKind.ExtensionMemberCref: | ||
| return HasParameterList(((ExtensionMemberCrefSyntax)crefSyntax).Member); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parameters on extension are intentionally not considered here.
The value from HasParameterList affects the result kind from GetCrefSymbolInfo below, to report "ambiguous" vs. "overload resolution failure". In E.extension(int).M... we only allow the last parameter list to be omitted, so the presence/absence of the parameter list on extension is not relevant to was result kind we report.
| N(SyntaxKind.CloseParenToken); | ||
| } | ||
| N(SyntaxKind.DotToken); | ||
| N(SyntaxKind.ExtensionMemberCref); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // void I.M() { } | ||
| Diagnostic(ErrorCode.ERR_ExplicitInterfaceImplementationInNonClassOrStruct, "M").WithArguments("Extensions.extension(object).M()").WithLocation(10, 16)); | ||
| Diagnostic(ErrorCode.ERR_ExplicitInterfaceImplementationInNonClassOrStruct, "M").WithArguments("Extensions.extension(object).M()").WithLocation(10, 16), | ||
| // (10,16): error CS9282: Extension declarations can include only methods or properties |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| public void Cref_44() | ||
| { | ||
| var src = """ | ||
| /// <see cref="extension(int).M"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that's possible. If extension(int) is interpreted as a reference to an extension block, then it's an error (shown). If it's interpreted as a named member cref, it would refer to a method and the .M would therefore fail to bind.
There was a problem hiding this comment.
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 a success scenario with syntax like that?
I don't think that's possible.
Even when it is used on or inside the static class containing the target extension block? Could you please add tests like that nonetheless
| /// <see cref="E.extension(int).M2"/> | ||
| static class E | ||
| { | ||
| extension(int i) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
Done with review pass (commit 1) #Closed |
| } | ||
|
|
||
| builder.Append(symbol.IsExtension ? symbol.ExtensionName : symbol.Name); | ||
| builder.Append(symbol.IsExtension ? escape(symbol.ExtensionName) : symbol.Name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like now one can refer to an extension type by name across assembly boundaries. I.e. doc comments for one assembly can refer to an extension type defined in a different assembly by using its unspeakable name. BTW, are we testing this scenario? Should a reference like this be considered a subject for a binary breaking change when a source change in the referenced assembly causes a change in the unspeakable name? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should a reference like this be considered a subject for a binary breaking change when a source change in the referenced assembly causes a change in the unspeakable name?
I have a follow-up to discuss with WG: "Are extension metadata names problematic for versioning docs?".
Yes, I think the current scheme means that shuffling extensions around breaks CREF/XML doc consumers.
| var comp = CreateCompilation(src, parseOptions: TestOptions.RegularPreviewWithDocumentationComments); | ||
| comp.VerifyEmitDiagnostics( | ||
| // (10,24): warning CS1574: XML comment has cref attribute 'M(string)' that could not be resolved | ||
| // /// <see cref="M(string)"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed with Mads over email thread
| // /// <see cref="E2.extension()"/> | ||
| Diagnostic(ErrorCode.WRN_BadXMLRef, "E2.extension()").WithArguments("extension()").WithLocation(9, 16), | ||
| // (10,16): warning CS1574: XML comment has cref attribute 'extension(int)' that could not be resolved | ||
| // /// <see cref="E2.extension(int)"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved in LDM today
| [Fact] | ||
| public void Cref_47() | ||
| { | ||
| // TODO2 error in Ioperation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| { | ||
| extension(int) | ||
| { | ||
| /// <see cref="extension(int).M2"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also on class E itself.
|
Done with review pass (commit 5) |
AlekseyTs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (commit 10)
| { | ||
| var src = """ | ||
| /// <see cref="E.extension(int).M(string)"/> | ||
| /// <see cref="E.M(int, string)"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider testing E.M(string) as well #Resolved
| private ImmutableArray<Symbol> BindExtensionMemberCref(ExtensionMemberCrefSyntax syntax, NamespaceOrTypeSymbol? containerOpt, out Symbol? ambiguityWinner, BindingDiagnosticBag diagnostics) | ||
| { | ||
| // Tracked by https://github.com/dotnet/roslyn/issues/76130 : handle extension operators | ||
| CheckFeatureAvailability(syntax, MessageID.IDS_FeatureExtensions, diagnostics); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a test for this langversion check? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See Cref_54
| Debug.Assert(CurrentToken.ContextualKind == SyntaxKind.ExtensionKeyword); | ||
|
|
||
| SyntaxToken identifierToken = EatToken(); | ||
| TypeArgumentListSyntax? typeArguments = (CurrentToken.Kind == SyntaxKind.LessThanToken) ? ParseTypeArguments(typeArgumentsMustBeIdentifiers: true) : null; |
There was a problem hiding this comment.
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 errors produced by typeArgumentsMustBeIdentifiers: true? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See Cref_43
Relates to test plan #76130
Review of public API: #78738